Navigation Menu
Site navigation bar whose triggers open panels of links below the header.
c2-navigation-menu
Site navigation bar: a row of c2-navigation-menu-item children, each either a plain link or a trigger that opens
a panel of c2-navigation-menu-link rows (or any markup) below the bar. The panels are native popovers on the top
layer positioned by c2-overlay, so they are never clipped by the header and light-dismiss on Escape or an outside
click.
The bar owns which panel is open: value is the value of that item ('' when closed), and value-change fires
on every change. Panels open on hover after open-delay and close after close-delay once the pointer leaves both
the item and its panel; a click on a trigger opens its panel straight away, and a click on the open trigger closes
it (hover then leaves that item shut until the pointer moves on). Set open-on="click" for a bar that ignores
hover entirely.
Keyboard: every item is a tab stop. On a trigger, ArrowLeft / ArrowRight and Home / End move along the bar (moving while a panel is open switches to that item's panel), ArrowDown, Enter or Space opens the panel and moves focus to its first link, and Escape closes it and returns focus to the trigger. Tab from an open trigger walks into the panel, since the popover sits right after the trigger in the DOM.
The bar never wraps: it is one row, and mode="mobile" is the other layout. In mobile mode the bar becomes a
single button — the mobile-trigger slot replaces it — that opens the whole navigation as one hierarchy: every
plain link is a row, and every item with a panel becomes a heading with its panel's rows underneath. Add
collapsible and those groups open one at a time instead, the open one being value, exactly as in the bar, and
the list starts on the group holding the current page. Set mobile-breakpoint and the bar switches modes on its
own below that viewport width; leave it unset and mode is yours to set, from a media query of your own or from
anything else that knows better.
Add auto-current to derive the current link from the browser URL. Exact hash links win, a hashless page link is
the fallback, and on a page made only of hash links the first row is the default. The menu follows hashchange and
popstate; SPA routers can set current-url or call syncCurrent(url) after navigation. Without auto-current,
authored current attributes remain fully controlled by the application.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
value | string | — | The value of the item whose panel is open; '' closes the bar. Set it to open a panel from code. |
aria-label | string | null | null | Accessible name of the navigation landmark. |
open-on | 'hover' | 'click' | hover | hover opens a panel on pointer intent (and on click); click waits for a click or the keyboard. |
open-delay | number | 150 | Pointer intent before the first panel opens, in milliseconds. A panel switch is always immediate. |
close-delay | number | 300 | Grace period before a panel closes once the pointer has left both the item and the panel, in milliseconds. |
panel-anchor | 'item' | 'menu' | item | item opens each panel under its own item; menu aligns every panel to the bar, for full-width mega panels. |
mode | NavigationMenuMode | bar | bar shows the row of items; mobile shows one button that opens the whole navigation as a hierarchy list. |
collapsible | boolean | false | In mobile mode, groups open one at a time instead of all being listed at once. |
mobile-breakpoint | number | 0 | Viewport width, in pixels, at or below which the bar switches itself to mobile — and back to bar above it.
Leave it at 0 to own mode yourself, which is what a breakpoint that depends on something other than the
window needs. |
mobile-label | string | Menu | Accessible name of the button shown in mobile mode. |
mobile-open | boolean | false | Whether the mobile button's list is showing. |
auto-current | boolean | false | Derives the current item or panel row from the browser URL and keeps it synchronized. |
current-url | string | — | Router-controlled URL used by auto-current; an empty value follows window.location. |
Slots
| Name | Description |
|---|---|
default | The items: c2-navigation-menu-item elements. |
mobile-trigger | Replaces the button shown in mobile mode. |
Events
| Name | Type | Description |
|---|---|---|
value-change | CustomEvent<ValueChangeEventDetail> | Fired after the open panel changes. detail.value is the value of the open item, or '' when the bar closed. |
CSS custom properties
Grouped by semantic target and state from --c2-<component>__<target>__<state>--<property>. Click a group to collapse it; click a name to copy it.
| Name | Type | Default | Description |
|---|---|---|---|
--c2-navigation-menu--gap | pixel | 4px | Space between the items. |
--c2-navigation-menu--background | color | — | |
--c2-navigation-menu--padding-top | padding | 0px | |
--c2-navigation-menu--padding-right | padding | 0px | |
--c2-navigation-menu--padding-bottom | padding | 0px | |
--c2-navigation-menu--padding-left | padding | 0px | |
--c2-navigation-menu--border-top-left-radius | border-radius | — | |
--c2-navigation-menu--border-top-right-radius | border-radius | — | |
--c2-navigation-menu--border-bottom-left-radius | border-radius | — | |
--c2-navigation-menu--border-bottom-right-radius | border-radius | — | |
--c2-navigation-menu--border-top | border | — | |
--c2-navigation-menu--border-right | border | — | |
--c2-navigation-menu--border-bottom | border | — | |
--c2-navigation-menu--border-left | border | — | |
--c2-navigation-menu--box-shadow | box-shadow | — | |
--c2-navigation-menu__mobile-trigger--size | pixel | 40px | |
--c2-navigation-menu__mobile-trigger--color | color | #18181b | |
--c2-navigation-menu__mobile-trigger--background | color | transparent | |
--c2-navigation-menu__mobile-trigger--border | border | 1px solid #e4e4e7 | |
--c2-navigation-menu__mobile-trigger--border-radius | border-radius | 8px | |
--c2-navigation-menu__mobile-trigger__icon--size | pixel | 20px | |
--c2-navigation-menu__mobile-trigger__hover--background | color | #f4f4f5 | |
--c2-navigation-menu__mobile-trigger__hover--color | color | — | |
--c2-navigation-menu__mobile-trigger__focus--outline | outline | 2px solid rgba(2, 101, 220, 0.4) | |
--c2-navigation-menu__mobile-trigger__focus--outline-offset | pixel | 1px | |
--c2-navigation-menu__mobile-panel--background | color | #ffffff | |
--c2-navigation-menu__mobile-panel--min-width | pixel | 260px | Set it to calc(100vw - 32px) for a sheet that spans the screen. |
--c2-navigation-menu__mobile-panel--max-width | pixel | — | |
--c2-navigation-menu__mobile-panel--max-height | pixel | min(80vh, var(--c2-overlay--available-height, 80vh)) | The list scrolls past this height. |
--c2-navigation-menu__mobile-panel--gap | pixel | 2px | Space between the rows and groups. |
--c2-navigation-menu__mobile-panel--border-top | border | 1px solid #e4e4e7 | |
--c2-navigation-menu__mobile-panel--border-right | border | 1px solid #e4e4e7 | |
--c2-navigation-menu__mobile-panel--border-bottom | border | 1px solid #e4e4e7 | |
--c2-navigation-menu__mobile-panel--border-left | border | 1px solid #e4e4e7 | |
--c2-navigation-menu__mobile-panel--border-top-left-radius | border-radius | 8px | |
--c2-navigation-menu__mobile-panel--border-top-right-radius | border-radius | 8px | |
--c2-navigation-menu__mobile-panel--border-bottom-left-radius | border-radius | 8px | |
--c2-navigation-menu__mobile-panel--border-bottom-right-radius | border-radius | 8px | |
--c2-navigation-menu__mobile-panel--box-shadow | box-shadow | 0 8px 24px rgba(24, 24, 27, 0.08) | |
--c2-navigation-menu__mobile-panel--padding-top | padding | 8px | |
--c2-navigation-menu__mobile-panel--padding-right | padding | 8px | |
--c2-navigation-menu__mobile-panel--padding-bottom | padding | 8px | |
--c2-navigation-menu__mobile-panel--padding-left | padding | 8px | |
c2-navigation-menu-item
One entry of a c2-navigation-menu. With an href and nothing in the panel slot it is a plain link; with content
in panel it becomes a trigger that opens that content below the bar, and the chevron and aria-expanded are
added for you. Mark the entry of the current page current to announce aria-current="page" and show the
indicator line.
A trigger follows its own panel: while one of the panel's rows is the current page — a current
c2-navigation-menu-link, or any element carrying aria-current — the item marks itself current-group and wears
the same styling as a current item, so the bar keeps showing which section the visitor is in while the panel is
closed. Only the row keeps aria-current: the trigger is the group, not the page.
The bar owns the open state: it sets expanded, hands the item the element its panel is anchored to, and handles
hover intent, the keyboard and dismissal. The panel itself is a c2-overlay popover, so it escapes any
overflow: hidden header, but a manual one: a bar that opens panels on hover cannot leave dismissal to the
browser, whose light dismiss then disagrees between engines about a press on the trigger of an open panel.
In the bar's mobile mode the item renders flat instead: a plain link stays a row, and a trigger becomes a heading
with its panel's rows listed underneath it, so the whole navigation reads as one hierarchy. With the bar's
collapsible set, that heading is a button that opens and closes its group.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
value | string | — | Identifies the item in the bar's value. The bar fills in item-<position> when the markup omits it. |
href | string | undefined | — | Makes the item a link. An item with panel content is always a trigger, even with an href. |
target | string | undefined | — | Browsing context used when href is set, such as _blank. |
current | boolean | false | The entry of the page being shown: announced as aria-current="page" and styled with the __current variables. |
disabled | boolean | false | Dims the item and ignores pointer and keyboard. |
label | string | undefined | — | Label used as the default slot's fallback content. |
expanded | boolean | false | Whether the panel is open. Set by the parent bar; announced as aria-expanded. |
current-group | boolean | false | Whether the current page is one of this item's panel rows. The item maintains it as the panel changes; style it
with the __current variables, or with the current-group attribute for a look of its own. |
placement | Placement | bottom-start | Preferred placement of the panel (floating-ui names). |
mobile | boolean | false | Set by the bar in mobile mode: the item becomes one entry of a hierarchy list — a row for a link, a heading
with its panel's rows underneath for a trigger — instead of a trigger with a popover. |
collapsible | boolean | false | Set by the bar's collapsible: in mobile mode the heading becomes a button that opens and closes the group. |
Slots
| Name | Description |
|---|---|
default | The label. Falls back to label. |
prefix-icon | Icon before the label. |
suffix-icon | Icon after the label. Defaults to a chevron that flips while the panel is open. |
panel | The panel content: c2-navigation-menu-link rows, or any markup you lay out yourself. |
Events
| Name | Type | Description |
|---|---|---|
panel-toggle | CustomEvent<PanelToggleEventDetail> | Fired after the panel opens or closes, including when the browser light-dismisses it. |
CSS custom properties
Grouped by semantic target and state from --c2-<component>__<target>__<state>--<property>. Click a group to collapse it; click a name to copy it.
| Name | Type | Default | Description |
|---|---|---|---|
--c2-navigation-menu-item--min-height | pixel | 36px | |
--c2-navigation-menu-item--gap | pixel | 6px | Space between the icons and the label. |
--c2-navigation-menu-item--padding-top | padding | 8px | |
--c2-navigation-menu-item--padding-right | padding | 12px | |
--c2-navigation-menu-item--padding-bottom | padding | 8px | |
--c2-navigation-menu-item--padding-left | padding | 12px | |
--c2-navigation-menu-item--border-top-left-radius | border-radius | 6px | |
--c2-navigation-menu-item--border-top-right-radius | border-radius | 6px | |
--c2-navigation-menu-item--border-bottom-left-radius | border-radius | 6px | |
--c2-navigation-menu-item--border-bottom-right-radius | border-radius | 6px | |
--c2-navigation-menu-item--font-size | font-size | 14px | |
--c2-navigation-menu-item--font-weight | font-weight | 500 | |
--c2-navigation-menu-item--font-style | font-style | — | |
--c2-navigation-menu-item--font-family | font-family | — | |
--c2-navigation-menu-item--color | color | #71717a | |
--c2-navigation-menu-item--background | color | transparent | |
--c2-navigation-menu-item__hover--color | color | #18181b | |
--c2-navigation-menu-item__hover--background | color | #f4f4f5 | |
--c2-navigation-menu-item__expanded--color | color | #18181b | |
--c2-navigation-menu-item__expanded--background | color | #f4f4f5 | |
--c2-navigation-menu-item__expanded--border-bottom-left-radius | border-radius | 0px | Square by default, so an open trigger and its panel read as one surface. |
--c2-navigation-menu-item__expanded--border-bottom-right-radius | border-radius | 0px | |
--c2-navigation-menu-item__current--color | color | #18181b | Also used while current-group is set. |
--c2-navigation-menu-item__current--font-weight | font-weight | 600 | |
--c2-navigation-menu-item__current--background | color | — | |
--c2-navigation-menu-item__indicator--color | color | rgb(2, 101, 220) | The line under an item that is current, holds the current row, or is expanded. Not drawn in the mobile list, where the current row is marked by __current--color. |
--c2-navigation-menu-item__indicator--height | pixel | 2px | |
--c2-navigation-menu-item__indicator--bottom | pixel | 0px | |
--c2-navigation-menu-item__indicator--inset | pixel | 0px | How far the line stops short of the item on each side; 0 spans its whole width. |
--c2-navigation-menu-item__indicator--border-radius | border-radius | 999px | |
--c2-navigation-menu-item__icon--size | pixel | 16px | |
--c2-navigation-menu-item__icon--color | color | — | Defaults to the label colour. |
--c2-navigation-menu-item__focus--outline | outline | 2px solid rgba(2, 101, 220, 0.4) | |
--c2-navigation-menu-item__focus--outline-offset | pixel | 1px | |
--c2-navigation-menu-item__disabled--opacity | opacity | 0.38 | |
--c2-navigation-menu-item__mobile-heading--color | color | #71717a | The group heading in the mobile list. |
--c2-navigation-menu-item__mobile-heading--font-size | font-size | 11px | |
--c2-navigation-menu-item__mobile-heading--font-weight | font-weight | 600 | |
--c2-navigation-menu-item__mobile-heading--letter-spacing | letter-spacing | 0.06em | |
--c2-navigation-menu-item__mobile-heading--text-transform | text-transform | uppercase | |
--c2-navigation-menu-item__mobile-heading--padding-top | padding | 10px | |
--c2-navigation-menu-item__mobile-heading--padding-right | padding | 8px | |
--c2-navigation-menu-item__mobile-heading--padding-bottom | padding | 4px | |
--c2-navigation-menu-item__mobile-heading--padding-left | padding | 8px | Also the inset of a top-level row in the mobile list, so the two line up. |
--c2-navigation-menu-item__mobile-heading__chevron--size | pixel | 16px | The toggle chevron of a collapsible group. |
--c2-navigation-menu-item__mobile-group--indent | pixel | 12px | How far the rows of a group are inset. |
--c2-navigation-menu-item__panel--background | color | #ffffff | |
--c2-navigation-menu-item__panel--min-width | pixel | 260px | |
--c2-navigation-menu-item__panel--max-width | pixel | — | |
--c2-navigation-menu-item__panel--max-height | pixel | min(70vh, var(--c2-overlay--available-height, 70vh)) | The panel scrolls past this height. |
--c2-navigation-menu-item__panel--border-top | border | 1px solid #e4e4e7 | |
--c2-navigation-menu-item__panel--border-right | border | 1px solid #e4e4e7 | |
--c2-navigation-menu-item__panel--border-bottom | border | 1px solid #e4e4e7 | |
--c2-navigation-menu-item__panel--border-left | border | 1px solid #e4e4e7 | |
--c2-navigation-menu-item__panel--border-top-left-radius | border-radius | 8px | |
--c2-navigation-menu-item__panel--border-top-right-radius | border-radius | 8px | |
--c2-navigation-menu-item__panel--border-bottom-left-radius | border-radius | 8px | |
--c2-navigation-menu-item__panel--border-bottom-right-radius | border-radius | 8px | |
--c2-navigation-menu-item__panel--box-shadow | box-shadow | 0 8px 24px rgba(24, 24, 27, 0.08) | |
--c2-navigation-menu-item__panel--padding-top | padding | 8px | |
--c2-navigation-menu-item__panel--padding-right | padding | 8px | |
--c2-navigation-menu-item__panel--padding-bottom | padding | 8px | |
--c2-navigation-menu-item__panel--padding-left | padding | 8px | |
c2-navigation-menu-link
A link row for the panel slot of a c2-navigation-menu-item: a title, an optional second line of description,
and optional icons. It renders a real <a>, so it is a tab stop, opens in a new tab with a modifier and reports
aria-current="page" when marked current. Lay several of them out with your own CSS grid to build a mega panel.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
href | string | undefined | — | Destination URL. Without it the row is rendered as non-navigating content. |
target | string | undefined | — | Browsing context used when href is set, such as _blank. |
current | boolean | false | The link to the page being shown: announced as aria-current="page" and styled with the __current variables. |
disabled | boolean | false | Dims the row and ignores clicks; the row stops being a link. |
label | string | undefined | — | Title used as the default slot's fallback content. |
Slots
| Name | Description |
|---|---|
default | The title. Falls back to label. |
description | Secondary line under the title (muted, smaller). |
prefix-icon | Icon before the title. |
suffix-icon | Icon after the title, e.g. an arrow for an outbound link. |
CSS custom properties
Grouped by semantic target and state from --c2-<component>__<target>__<state>--<property>. Click a group to collapse it; click a name to copy it.
| Name | Type | Default | Description |
|---|---|---|---|
--c2-navigation-menu-link--gap | pixel | 10px | Space between the icons and the text. |
--c2-navigation-menu-link--padding-top | padding | 8px | |
--c2-navigation-menu-link--padding-right | padding | 10px | |
--c2-navigation-menu-link--padding-bottom | padding | 8px | |
--c2-navigation-menu-link--padding-left | padding | 10px | |
--c2-navigation-menu-link--border-top-left-radius | border-radius | 6px | |
--c2-navigation-menu-link--border-top-right-radius | border-radius | 6px | |
--c2-navigation-menu-link--border-bottom-left-radius | border-radius | 6px | |
--c2-navigation-menu-link--border-bottom-right-radius | border-radius | 6px | |
--c2-navigation-menu-link--font-size | font-size | 14px | |
--c2-navigation-menu-link--font-weight | font-weight | 500 | |
--c2-navigation-menu-link--line-height | pixel | 20px | |
--c2-navigation-menu-link--color | color | #18181b | |
--c2-navigation-menu-link--background | color | transparent | |
--c2-navigation-menu-link__hover--background | color | #f4f4f5 | |
--c2-navigation-menu-link__hover--color | color | — | Defaults to the title colour. |
--c2-navigation-menu-link__current--color | color | rgb(2, 101, 220) | |
--c2-navigation-menu-link__current--background | color | #edf1fe | |
--c2-navigation-menu-link__description--color | color | #71717a | |
--c2-navigation-menu-link__description--font-size | font-size | 12px | |
--c2-navigation-menu-link__description--line-height | pixel | 16px | |
--c2-navigation-menu-link__description--font-weight | font-weight | 400 | |
--c2-navigation-menu-link__description--margin-top | pixel | 2px | |
--c2-navigation-menu-link__icon--size | pixel | 18px | |
--c2-navigation-menu-link__icon--color | color | #71717a | |
--c2-navigation-menu-link__focus--outline | outline | 2px solid rgba(2, 101, 220, 0.4) | |
--c2-navigation-menu-link__focus--outline-offset | pixel | -2px | |
--c2-navigation-menu-link__disabled--opacity | opacity | 0.38 | |