コンテンツにスキップ

メニュー

hc-menu は、ネイティブの popover を WAI-ARIA のアクションメニューと してスタイルします。トリガーは普通のボタンです。popovertarget 属性が JS なしで両者をつなぎ、installMenu() がキーボードパターン、 ARIA 配線、hc:menuselect イベントを足します。

別名: ドロップダウン、ドロップダウンメニュー。

プリミティブ必要バージョン
HTML popoverChrome 114、Edge 114、Firefox 125、Safari 17
CSS Anchor PositioningChrome 125、Edge 125、Firefox 147、Safari 26

Anchor Positioning がない場合、installMenu() は popover の beforetoggle イベントで getBoundingClientRect によるメニュー配置に フォールバックします。popover がサポートされる環境ならどこでも、 メニューは正しく開き、閉じ、振る舞います。

import { installMenu } from '@hypermedia-components/core';
installMenu();

installMenu() は冪等で、アンインストーラを返します。ゼロ設定の @hypermedia-components/core/behaviors エントリが自動インストールし、 htmx でスワップされた内容も自動で拾います。

  • トリガーへの ARIA 配線: aria-haspopup="menu"aria-expanded (popover の toggle イベントで同期)、aria-controls
  • 最初の有効なメニュー項目に autofocus を足し、ブラウザの popover アルゴリズムが開いたときにそこへフォーカスするようにします — ブラウザ自身のフォーカス管理と競合する JS はありません。
  • 対応する anchor-name(トリガー)と position-anchor(メニュー)を 注入し、CSS Anchor Positioning でメニューがトリガーの直下に着地 します。
  • APG のキーボードパターンを実装します:
    • ArrowDown / ArrowUp は有効な項目の間でフォーカスを移動し、 端でラップします。
    • Home / End は最初 / 最後の有効な項目へジャンプ。
    • 1 文字キーは、その文字で始まる次の項目へジャンプ (タイプアヘッド)。
    • Tab はメニューを閉じ、フォーカスはトリガーの次のタブストップへ 続きます。
    • Escape と外側クリックは popover がネイティブに処理します。
  • disabled または aria-disabled="true" の項目をスキップします。
  • menuitem のクリック時に、detail{ item, menu, trigger } を 運ぶバブリングする hc:menuselect カスタムイベントを発火し、 その後 hidePopover() でメニューを閉じます。

状態を持つ項目(チェックボックス + ラジオ)

Section titled “状態を持つ項目(チェックボックス + ラジオ)”

2 つの追加 ARIA role により、メニュー項目は単発アクションの発火では なく状態を運べます:

  • role="menuitemcheckbox" — 独立したオン / オフ。複数同時に チェックできます。クリックが aria-checked をトグルします。
  • role="menuitemradio" — グループ内で相互排他。クリックがこの項目の aria-checked="true" を設定し、すべての兄弟を "false" にします。 グループの境界は最寄りの [role="group"] 祖先で、なければメニュー 自体です。

どちらもクリック後にメニューを開いたまま保ちます(shadcn / Radix の 作法 — ユーザーは通常、開き直さずに複数のオプションをトグルします)。 hc:menuselect イベントは引き続き発火し、その detail.checked が 新しい真偽値状態を運びます。

<span class="hc-menu__label"> 要素はグループの上に小さなミュートの 見出しを描画します。囲んでいる <div role="group">aria-labelledby と対にしてください。

document.getElementById('view-menu').addEventListener('hc:menuselect', (e) => {
const { item, checked } = e.detail;
console.log(item.textContent.trim(), '', checked);
// e.g. 'Sidebar → true'
});

メニューにチェックボックスまたはラジオ項目が1 つでも含まれると、 メニュー内のすべての項目が左側に予約されたインジケーター列を得るため、 素の menuitem もチェック / ドットのマーカーと揃います。この列は 純粋に CSS の :has() で有効になります — マークアップの変更は不要 です。

shadcn の destructive バリアントに倣い、メニュー項目の data-variant="error"--hc-menu-item-error-fg でテキストを 塗り替えます。

<button class="hc-menu__item" role="menuitem" type="button"
data-variant="error">
Delete
</button>

メニュー項目はサブメニュー — それが制御する入れ子の .hc-menu — を開けます。サブメニューをルートメニューの DOM 内に入れ子にし、親の 項目から data-hc-submenu="<submenu-id>" で指します。 installMenu()popover="auto" とサブメニューの ARIA を自動で 足します。同じ配線が hc-context-menuの サブメニューも無料で駆動します。

サブメニューをルートの popover 内に入れ子にすることが、サブメニュー 表示中もルートを開いたまま保つ当のものです(HTML popover の 「入れ子」ルール)。

インタラクション(WAI-ARIA APG のサブメニューパターン):

操作結果
親をホバーサブメニューが開く(フォーカスは動かない)。
クリック、EnterSpace、または 開いて最初の項目にフォーカス。
サブメニューを閉じ、フォーカスは親へ戻る。
Escサブメニューを閉じる(次にルート)。
兄弟をホバー開いているサブメニューを閉じる。
任意の末端を選択ツリー全体を閉じる。

RTL では開閉の矢印がミラーされます。親項目は後置のシェブロン (::after)を表示します。配置は CSS Anchor Positioning (サブメニューは inline-end へ、端では inline-start へ反転)で、 それがないエンジンにはドロップダウンと同じ JS フォールバックが あります。親の aria-haspopup="menu"aria-expandedaria-controls は同期され続けます。

メニュー項目は hc:menuselect イベント経由で htmx リクエストを発火 できます。ビヘイビアは発火後にメニューを閉じるため、以後のネットワーク 処理はフォーカスがトリガーへ復元された状態(popover のデフォルト)で 行われます。

<tr>
<td>Item 42</td>
<td>
<button class="hc-button" type="button"
popovertarget="row-menu" id="row-menu-trigger">
Actions
</button>
<div class="hc-menu" id="row-menu" popover role="menu"
aria-labelledby="row-menu-trigger"
data-hx-target="closest tr"
data-hx-trigger="hc:menuselect"
data-hx-include="this">
<button class="hc-menu__item" role="menuitem" type="button"
data-hx-delete="/items/42">Delete row</button>
</div>
</td>
</tr>

より豊かなパターン(実リクエストの前の確認ダイアログ)には、各 メニュー項目を hc-confirm-actionと 組み合わせてください。

  • メニューは常に、トリガーの id を指す aria-labelledby で トリガーと関連付けてください。
  • すべてのメニュー項目に role="menuitem" が必要です。tabindex なしで到達可能になるよう、本物の <button>(ナビゲーションメニュー なら <a>)を使ってください。
  • 項目の無効化はネイティブの disabled 属性または aria-disabled="true" のどちらかで。どちらも矢印ナビゲーションから スキップされ、hc:menuselect のクリックから無視されます。
  • フォーカスアウトラインを上書きしないでください。アクティブな項目は :focus-visible 経由で同じフォーカス背景を受け取ります。
  • menuitemcheckbox / menuitemradio の項目は aria-checked で 自身のチェック状態を保ちます(状態を持つ項目を 参照)。トグル後もメニューは開いたままです。変更の永続化には hc:menuselect イベントを使ってください。サブメニューは data-hc-submenu でサポートされます(サブメニューを 参照)。

component トークン(component.tokens.json):

トークンパス用途
menu.bg / fg / border / radiusメニューの面。
menu.padding-block項目リスト周りの縦パディング。
menu.min-width / max-width面の制約。
menu.offsetトリガーとメニューの距離(Anchor Positioning)。
menu.item.padding-x / padding-y / font-size / gap項目のレイアウト。
menu.item.fg / hover-bg / focus-bg / disabled-fg項目の状態。
menu.item.error-fgdata-variant="error" の前景色。
menu.item.indicator-sizeチェック / ドット列のサイズ。
menu.label.padding-x / padding-y / font-size / font-weight / fg<span class="hc-menu__label"> グループ見出し。
menu.separator.color / margin-y<hr class="hc-menu__separator">
生成される CSS 変数を表示
--hc-menu-bg | -fg | -border | -radius | -padding-block
--hc-menu-min-width | -max-width | -offset
--hc-menu-item-padding-x | -padding-y | -font-size | -gap
--hc-menu-item-fg | -hover-bg | -focus-bg | -disabled-fg
--hc-menu-item-error-fg | -indicator-size
--hc-menu-label-padding-x | -padding-y | -font-size | -font-weight | -fg
--hc-menu-separator-color | -margin-y
--hc-color-focus-ring (inherited from data-color)
  • ポップオーバー — メニューパターンなしの素のポップオーバー面。
  • ダイアログ — より豊かなフローのためのモーダルな代替。
  • ツールバー — メニュートリガーとよく組み合う水平のアクションクラスター。

レシピでの利用: 保存ビュー