ナビゲーションメニュー
stimeo--navigation-menu
サイトのナビゲーション。リンクを並べたパネルを 1 つずつ開きます。アプリのメニューではありません。
サイトのナビゲーションです。上の各ボタンがリンクを並べたパネルを開き、開くのは一度に 1 つだけです。中身はただのリンクなので、フォーカスは閉じ込められず、Tab でそのまま辿れます。Esc で開いているパネルを閉じてボタンへフォーカスを戻し、外側のクリックやナビゲーションからフォーカスが出たときも閉じます。左右キーはボタンの間を移動しますが、Tab の順番は自然なままです。ホバーで開く動きは、必要なときだけ遅延つきで有効にできます。矢印キーで渡り歩くアプリのコマンドメニューが要るなら、メニューバーを使ってください。
実行中
キーボード操作
| キー | 動作 |
|---|---|
| → / ← | トップ項目間でフォーカスを移動(ループ)。RTL では左右が入れ替わる。 |
| Esc | 開いているパネルを閉じてトリガーへフォーカスを戻す。 |
| Tab | 自然に移動。ナビ外へ出たら開いているパネルを閉じる。 |
<%# Markup for the navigation-menu demo.
Two arrangements are shown.
1. Click-only (the default): a global nav where each top item opens/closes a
sub-panel (a set of links). The contents are links and don't use role="menu"
(APG Disclosure navigation). The library handles syncing aria-expanded,
single-open behavior, closing on Escape / outside click / focus leaving, and
moving between top items with the left/right keys.
2. Hover (opt-in): openOnHover plus a hoverArea wrapper — the APG "Disclosure
Navigation with Top-Level Links" arrangement, where each <li> holds a real
link, a small disclosure button, and the panel. Hovering anywhere over the
<li> opens that item's panel; every keyboard path stays identical.
Panel placement and styling are the consumer's CSS (demo.css). %>
<p class="nav-menu__caption"><%= t("components.navigation_menu.demo.click_caption") %></p>
<nav class="nav-menu" data-controller="stimeo--navigation-menu"
aria-label="<%= t("components.navigation_menu.demo.label") %>">
<ul class="nav-menu__list" data-stimeo--navigation-menu-target="list">
<li class="nav-menu__item">
<button
type="button"
class="demo-baritem"
aria-expanded="false"
aria-controls="nav-menu-products"
data-stimeo--navigation-menu-target="trigger"
data-action="click->stimeo--navigation-menu#toggle
keydown->stimeo--navigation-menu#onTriggerKeydown">
<%= t("components.navigation_menu.demo.products.label") %>
</button>
<div id="nav-menu-products" class="nav-menu__panel"
data-stimeo--navigation-menu-target="panel" hidden>
<a class="demo-menuitem" href="#analytics"><%= t(
"components.navigation_menu.demo.products.links.analytics"
) %></a>
<a class="demo-menuitem" href="#automation"><%= t(
"components.navigation_menu.demo.products.links.automation"
) %></a>
<a class="demo-menuitem" href="#reports"><%= t(
"components.navigation_menu.demo.products.links.reports"
) %></a>
</div>
</li>
<li class="nav-menu__item">
<button
type="button"
class="demo-baritem"
aria-expanded="false"
aria-controls="nav-menu-company"
data-stimeo--navigation-menu-target="trigger"
data-action="click->stimeo--navigation-menu#toggle
keydown->stimeo--navigation-menu#onTriggerKeydown">
<%= t("components.navigation_menu.demo.company.label") %>
</button>
<div id="nav-menu-company" class="nav-menu__panel"
data-stimeo--navigation-menu-target="panel" hidden>
<a class="demo-menuitem" href="#about"><%= t(
"components.navigation_menu.demo.company.links.about"
) %></a>
<a class="demo-menuitem" href="#careers"><%= t(
"components.navigation_menu.demo.company.links.careers"
) %></a>
</div>
</li>
</ul>
</nav>
<p class="nav-menu__caption"><%= t("components.navigation_menu.demo.hover_caption") %></p>
<%# openOnHover is opt-in; hoverArea widens the hover region to the whole <li>, so
hovering the top-level link (not just the disclosure button) opens the panel.
The button keeps its own accessible name so keyboard and SR users can tell the
link and its disclosure apart. %>
<nav class="nav-menu" data-controller="stimeo--navigation-menu"
data-stimeo--navigation-menu-open-on-hover-value="true"
aria-label="<%= t("components.navigation_menu.demo.hover.label") %>">
<ul class="nav-menu__list" data-stimeo--navigation-menu-target="list">
<li class="nav-menu__item" data-stimeo--navigation-menu-target="hoverArea">
<a class="demo-baritem nav-menu__link" href="#solutions">
<%= t("components.navigation_menu.demo.hover.solutions.label") %>
</a>
<button
type="button"
class="demo-baritem nav-menu__disclosure"
aria-expanded="false"
aria-controls="nav-menu-solutions"
data-stimeo--navigation-menu-target="trigger"
data-action="click->stimeo--navigation-menu#toggle
keydown->stimeo--navigation-menu#onTriggerKeydown">
<span aria-hidden="true">▾</span>
<span class="visually-hidden"><%= t(
"components.navigation_menu.demo.hover.solutions.toggle"
) %></span>
</button>
<div id="nav-menu-solutions" class="nav-menu__panel"
data-stimeo--navigation-menu-target="panel" hidden>
<a class="demo-menuitem" href="#retail"><%= t(
"components.navigation_menu.demo.hover.solutions.links.retail"
) %></a>
<a class="demo-menuitem" href="#finance"><%= t(
"components.navigation_menu.demo.hover.solutions.links.finance"
) %></a>
</div>
</li>
<li class="nav-menu__item" data-stimeo--navigation-menu-target="hoverArea">
<a class="demo-baritem nav-menu__link" href="#resources">
<%= t("components.navigation_menu.demo.hover.resources.label") %>
</a>
<button
type="button"
class="demo-baritem nav-menu__disclosure"
aria-expanded="false"
aria-controls="nav-menu-resources"
data-stimeo--navigation-menu-target="trigger"
data-action="click->stimeo--navigation-menu#toggle
keydown->stimeo--navigation-menu#onTriggerKeydown">
<span aria-hidden="true">▾</span>
<span class="visually-hidden"><%= t(
"components.navigation_menu.demo.hover.resources.toggle"
) %></span>
</button>
<div id="nav-menu-resources" class="nav-menu__panel"
data-stimeo--navigation-menu-target="panel" hidden>
<a class="demo-menuitem" href="#guides"><%= t(
"components.navigation_menu.demo.hover.resources.links.guides"
) %></a>
<a class="demo-menuitem" href="#blog"><%= t(
"components.navigation_menu.demo.hover.resources.links.blog"
) %></a>
</div>
</li>
</ul>
</nav>
<%# Readout: what the component just reported. Purely visual — the component
already announces what a screen reader needs, so this line is aria-hidden to
keep it from being read twice. The value is a code fragment (API identifiers
and values), so only the label is translated. %>
<p class="demo-readout" aria-hidden="true">
<span><%= t("catalog.readout.events") %></span>
<span class="demo-readout__value" data-readout>—</span>
</p>
/*
* Presentation-only styles for the navigation-menu demo.
* The library toggles the panel's hidden and the top items' aria-expanded. Panel
* placement (directly below the trigger) is static and the consumer's CSS
* responsibility; use stimeo-ui/positioning for dynamic flip.
*/
/* Caption naming each arrangement (click-only, then the opt-in hover one). */
.nav-menu__caption {
margin: 0 0 0.5rem;
font-size: 0.85rem;
color: var(--color-text-muted);
}
/* Space the second arrangement away from the first nav's panels. */
.nav-menu + .nav-menu__caption {
margin-top: 1.75rem;
}
.nav-menu__list {
display: flex;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
}
.nav-menu__item {
position: relative;
}
/* Top-level-links arrangement: a real link next to a small disclosure button,
both inside the <li> that acts as the hoverArea. */
/* Trimmed on the end side so the link and its disclosure button read as one pair. */
.nav-menu__link {
padding-inline-end: 0.25rem;
}
/* Narrower than a labelled bar item — it carries only the chevron — but the same
block padding, so it lines up with the link it belongs to. */
.nav-menu__disclosure {
padding-inline: 0.5rem;
}
.nav-menu__panel {
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
z-index: 10;
display: flex;
flex-direction: column;
min-width: 12rem;
padding: 0.5rem;
background: var(--surface-card);
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}
.nav-menu__panel[hidden] {
display: none;
}
// Every move of the open state is reported, with a `reason` saying what drove it
// — a trigger, Escape, a click outside, or a call from your own code. A real app
// subscribes to mirror the state elsewhere or to send an analytics event; this
// demo writes the event name and its reason into the readout so both are visible
// while you interact. `reason: "api"` is the one to watch for: a close you asked
// for comes back to your own listener.
// The page chrome uses some of these controllers too (the nav, the tab strip),
// so everything below is scoped to the demo itself.
const demoStage = document.querySelector(".demo-stage") ?? document;
const stateReadout = demoStage.querySelector("[data-readout]");
const stateRoot = demoStage.querySelector("[data-controller~='stimeo--navigation-menu']");
for (const name of ["open", "close"]) {
stateRoot?.addEventListener(`stimeo--navigation-menu:${name}`, (event) => {
const reason = event.detail?.reason;
if (stateReadout) {
stateReadout.textContent = reason
? `stimeo--navigation-menu:${name} · reason: ${reason}`
: `stimeo--navigation-menu:${name}`;
}
});
}
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--navigation-menu"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
trigger
必須
|
サブパネルを開閉するトップレベルのディスクロージャーボタン。aria-expanded が同期される。 |
data-stimeo--navigation-menu-target="trigger" |
panel
必須
|
トリガーが制御するリンクのサブパネル(aria-controls/id)。hidden 状態が同期される。 |
data-stimeo--navigation-menu-target="panel" |
hoverArea
|
ホバー反応域を内包要素全体へ広げる任意のラッパー(例: トップレベルのリンク・トリガー・パネルを含む <li>)。ホバーすると内包するトリガーのパネルを開閉する。1 つのラッパーに内包するトリガーは 1 つだけにする。openOnHover が有効なときだけ働く。 |
data-stimeo--navigation-menu-target="hoverArea" |
list
|
トップレベルのトリガーを並べる任意の容器。左右キーでどちらが次のトリガーかは、この容器の computed direction で決まる。置かない場合はコントローラ要素を読む。 | data-stimeo--navigation-menu-target="list" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
openOnHover
|
ホバーでパネルを開閉するか(オプトイン)。既定値は false。実行時に変更するとその場で配線・解除される。ポインタがそのトリガーの領域に留まっている間、開いているパネルのトリガーへのクリックは何もしない(閉じるとポインタが領域外へ出て戻るまで再オープンできなくなるため)。 | data-stimeo--navigation-menu-open-on-hover-value |
hoverDelay
|
ホバーでパネルを開閉するまでの遅延(ミリ秒)。既定値は 150。 | data-stimeo--navigation-menu-hover-delay-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
onTriggerKeydown
|
ArrowLeft/ArrowRight でトリガー間のフォーカスを移動する。自然な Tab 順は維持する。hidden や disabled のトリガーは飛ばし、修飾キー付きの矢印(Alt+← / Alt+→)はブラウザに渡す。 | stimeo--navigation-menu#onTriggerKeydown |
toggle
|
クリックされたトリガーのパネルを開閉する(同時に一つだけ開く)。保留中のホバー予約は打ち消す。 | stimeo--navigation-menu#toggle |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
close
|
パネルを閉じた直後に発火。detail に reason と、動いたパネルの index / panel を載せる。 | stimeo--navigation-menu:close |
open
|
パネルを開いた直後に発火。detail に reason と、動いたパネルの index / panel を載せる。 | stimeo--navigation-menu:open |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-expanded |
トリガー | そのパネルの開閉状態(true/false)。 |
hidden |
パネル | 閉じているときは付与。 |