Eunomia v1.1.0
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Nav menu

Nav menu groups related pages under a single entry of your site's navigation, such as "Our firm" leading to the team, the fees and the office. Visitors open it by hovering or clicking the entry, and every page listed inside is a real link, so it can be bookmarked, shared, opened in a new tab and found by search engines. The entry itself shows when one of its pages is the one being visited.

Dependencies

eun-icon eun-link · recommended for the links
Overview API Examples Accessibility

When to use

Use a nav menu in a site's main navigation when several pages belong together and listing all of them in the bar would crowd it, such as the pages of a section or the different services of a business. It's for going somewhere: every entry of the panel is a page. To offer actions on the current page instead (edit, duplicate, delete), use a dropdown button. On a phone, where there's no room for a floating panel, list the same links in an collapse inside the toolbar's mobile menu (see "Mobile menu" in the Examples tab).

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/nav-menu.js";
import "@eunomia/elements/link.js";
import "@eunomia/elements/icon.js";
<eun-nav-menu label="Our firm">
  <eun-link href="/firm/team">Our team</eun-link>
  <eun-link href="/firm/fees">Fees</eun-link>
  <eun-link href="/firm/office">The office</eun-link>
</eun-nav-menu>

Importing the file registers <eun-nav-menu> as a custom element, with no further setup needed. Each direct child is one link of the panel: an eun-link, a plain <a>, or your framework's own link component. eun-icon draws the trigger's chevron, so import it alongside.

Alternatives

You want… Prefers
A menu of actions on the current page, such as Edit or Delete, rather than pages to visit eun-dropdown-button
A group of links that expands in place, pushing the content below, such as in a mobile menu eun-collapse
A single page, with nothing to group under it eun-link

Guidance

  • Give the trigger a short label that names the group, such as "Our firm" or "Services", so visitors know what they'll find before opening it
  • Write the links with eun-link or your framework's own link component, so they stay real links and keep client-side navigation
  • Mark the current page on its link, with current or aria-current="page": the trigger follows on its own
  • Keep the panel to a handful of pages, around two to seven
  • Putting buttons or actions in the panel: that's what eun-dropdown-button is for
  • Making the group's own landing page reachable only through the trigger: the trigger opens the panel, it never navigates, so add that page as the first link of the panel
  • Wrapping the links in another element, such as a <div> or a fragment that renders one: each direct child is a link, so the wrapper would become a single entry
  • Nesting a nav menu inside another one

Live testing

Properties

eun-nav-menu only needs a label and its links as children. Everything else is optional.

Nav menu <eun-nav-menu>

Attributes

NameTypeDefaultDescription
show-delaynumber100Milliseconds the pointer rests on the trigger before the panel opens, with trigger set to hover
hide-delaynumber200Milliseconds before the panel closes once the pointer leaves it, long enough to move from the trigger into the panel, with trigger set to hover
aria-labelstring—The accessible label of the trigger, required when it has no visible label, such as an icon only trigger
labelstring—The trigger's text. Ignored once something is slotted into label
iconEunomiaIconName—An optional decorative icon shown before the label. Ignored once the icon slot has content
chevron-iconEunomiaIconName'keyboard_arrow_down'The trailing chevron icon. Ignored once the chevron slot has content
hide-chevronbooleanfalseHides the trailing chevron, leaving the label alone in the trigger
trigger'hover' | 'click''hover'What opens the panel. With hover, it opens when the pointer rests on the trigger and a click keeps it open. With click, only a click opens and closes it. Touch screens and the keyboard always use the click behavior
placement'bottom-start' | 'bottom' | 'bottom-end' | 'top-start' | 'top' | 'top-end''bottom-start'Where the panel opens relative to the trigger. It flips to the other side on its own when there isn't enough room
openbooleanfalseWhether the panel is currently open
currentbooleanfalseShows the trigger as the current section. Already set on its own when one of the links is the current page, so only needed when the current page isn't one of them
theme'light' | 'dark''light'The color theme of the trigger and the panel. Use dark on a dark background, such as a dark toolbar

Import the exact TypeScript type behind any property above, see Types.

Slots

NameDescription
(default)The links, one per direct child : an eun-link, which fills its row on its own, a native anchor, or a framework's own link component
labelThe trigger's label as light DOM content, so it's part of the server-rendered HTML. Falls back to label when empty
iconReplaces the trigger's leading icon entirely with any content, not just an icon element. Falls back to icon when empty
chevronReplaces the trigger's trailing chevron entirely with any content, not just an icon element. Falls back to chevronIcon when empty. Whatever ends up there still rotates while open, unless that rotation is set to zero

Events

NameTypeDescription
eunchangeChangeEventFired whenever the panel opens or closes from an interaction. Read the new state from open

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--nav-menu-trigger-colorSets the text and icon color of the trigger
--nav-menu-trigger-color-hoverSets the text and icon color of the trigger on hover, on keyboard focus and while open
--nav-menu-trigger-color-currentSets the text and icon color of the trigger when it's the current section
--nav-menu-trigger-backgroundSets the background of the trigger, transparent by default
--nav-menu-trigger-background-hoverSets the background of the trigger on hover, on keyboard focus and while open
--nav-menu-trigger-shadowSets the box shadow of the trigger, none by default
--nav-menu-trigger-shadow-hoverSets the box shadow of the trigger on hover, on keyboard focus and while open, falling back to --nav-menu-trigger-shadow
--nav-menu-trigger-font-sizeSets the font size of the trigger, inherited from the surrounding text by default
--nav-menu-trigger-font-weightSets the font weight of the trigger
--nav-menu-trigger-paddingSets the padding of the trigger
--nav-menu-trigger-padding-blockSets the vertical padding of the trigger, as one length applied to both sides. Ignored when --nav-menu-trigger-padding is set
--nav-menu-trigger-padding-inlineSets the horizontal padding of the trigger, as one length applied to both sides. Ignored when --nav-menu-trigger-padding is set
--nav-menu-trigger-gapSets the gap between the trigger's icon, label and chevron
--nav-menu-trigger-border-radiusSets the corner radius of the trigger and its focus ring
--nav-menu-focus-outline-colorSets the color of the trigger's focus ring, and of plain anchors in the panel
--nav-menu-icon-sizeSets the size of the leading icon and the chevron, 1.25em by default so they follow the font size
--nav-menu-chevron-rotationSets the rotation applied to the chevron while open, 180deg by default. Zero disables it
--nav-menu-panel-backgroundSets the background of the panel
--nav-menu-panel-border-colorSets the border color of the panel
--nav-menu-panel-border-radiusSets the corner radius of the panel
--nav-menu-panel-shadowSets the box shadow of the panel
--nav-menu-panel-paddingSets the padding of the panel
--nav-menu-panel-gapSets the gap between the links of the panel
--nav-menu-panel-min-widthSets the minimum width of the panel, which is never narrower than the trigger
--nav-menu-panel-max-heightSets the maximum height of the panel, beyond which it scrolls
--nav-menu-offsetSets the distance between the trigger and the panel
--nav-menu-link-colorSets the color of the links in the panel
--nav-menu-link-color-hoverSets the color of the links in the panel on hover and focus
--nav-menu-link-color-currentSets the color of the current page's link in the panel
--nav-menu-link-background-hoverSets the background of the links in the panel on hover and focus
--nav-menu-link-paddingSets the padding of the links in the panel
--nav-menu-link-border-radiusSets the corner radius of the links in the panel
--nav-menu-transition-durationSets the duration of the panel's open and close animation, the chevron's rotation and the color transitions

CSS shadow parts

NameDescription
triggerThe trigger button, for effects its custom properties don't cover, such as an active indicator
panelThe floating panel holding the links

Basic

Our team Fees The office
<eun-nav-menu label="Our firm">
  <eun-link href="/firm/team">Our team</eun-link>
  <eun-link href="/firm/fees">Fees</eun-link>
  <eun-link href="/firm/office">The office</eun-link>
</eun-nav-menu>

Links

With eun-link

eun-link is the recommended link. The menu styles it as a menu row on its own: it fills the whole row, which takes the hover, the focus ring and the click, and it has no underline, a hover background and no visited color. A label too long for the panel is cut with an ellipsis. There's no need for block here, the menu applies the same layout itself.

With icons

eun-link's own icon shows before each page's label.

Legal advice Contracts Litigation
<eun-nav-menu label="Services">
  <eun-link href="/services/advice" icon="gavel">Legal advice</eun-link>
  <eun-link href="/services/contracts" icon="description">Contracts</eun-link>
  <eun-link href="/services/litigation" icon="balance">Litigation</eun-link>
</eun-nav-menu>

Plain anchors

A plain <a> works too, with the same row look.

<eun-nav-menu label="Our firm">
  <a href="/firm/team">Our team</a>
  <a href="/firm/fees">Fees</a>
  <a href="/firm/office">The office</a>
</eun-nav-menu>

External link

An eun-link with target="_blank" shows its external icon and announces the new tab, inside the panel like anywhere else.

Guides Légifrance
<eun-nav-menu label="Resources">
  <eun-link href="/guides">Guides</eun-link>
  <eun-link href="https://www.legifrance.gouv.fr" target="_blank">
    Légifrance
  </eun-link>
</eun-nav-menu>

Current page

From the links

Mark the current page on its own link, with current on an eun-link or aria-current="page" on an anchor. The trigger then takes the current color too, and announces it, so visitors see which section they're in before opening the menu.

Our team Fees The office
<eun-nav-menu label="Our firm">
  <eun-link href="/firm/team">Our team</eun-link>
  <eun-link href="/firm/fees" current>Fees</eun-link>
  <eun-link href="/firm/office">The office</eun-link>
</eun-nav-menu>

Set by hand

When the current page belongs to the section without being one of its links, such as an article under "News", set current on the menu itself.

Latest articles Archive
<eun-nav-menu label="News" current>
  <eun-link href="/news">Latest articles</eun-link>
  <eun-link href="/news/archive">Archive</eun-link>
</eun-nav-menu>

Trigger

With an icon

icon shows a decorative icon before the label.

Profile Billing
<eun-nav-menu label="Account" icon="person">...</eun-nav-menu>

Without a chevron

hide-chevron removes the chevron, leaving the label alone, for a navigation where every entry should look the same.

Our team Fees
<eun-nav-menu label="Our firm" hide-chevron>...</eun-nav-menu>

Custom chevron

chevron-icon swaps the chevron for any icon, and --nav-menu-chevron-rotation sets how far it turns while open, 180deg by default. Zero keeps it still. For anything an icon can't express, slot your own non-interactive content into chevron.

Our team Fees Our team Fees
<eun-nav-menu
  label="Our firm"
  chevron-icon="chevron_right"
  style="--nav-menu-chevron-rotation: 90deg;"
>
  ...
</eun-nav-menu>

Rich label

The label slot takes any inline content, and puts the trigger's text in the server HTML. Keep it to text and inline phrasing, since it renders inside the trigger button.

Our firm Our team Fees
<eun-nav-menu>
  <span slot="label">Our <strong>firm</strong></span>
  ...
</eun-nav-menu>

Icon only

A trigger without visible text needs an aria-label, forwarded to the trigger button, so it still has an accessible name.

Mail Calendar
<eun-nav-menu icon="apps" aria-label="Our apps" hide-chevron>...</eun-nav-menu>

Opening

On hover

By default the panel opens once the pointer rests on the trigger for show-delay (100ms), and closes hide-delay (200ms) after it leaves both the trigger and the panel. That delay is what lets the pointer travel from the trigger into the panel without closing it. A click keeps the panel open wherever the pointer goes, and a second click closes it. Touch screens and the keyboard have no hover, so they always open and close it on a tap or a key press.

Our team Fees
<eun-nav-menu label="Our firm" show-delay="150" hide-delay="400"
  >...</eun-nav-menu
>

On click only

trigger="click" ignores the hover: only a click opens and closes the panel. Prefer it when the menu sits next to content the pointer often passes over, or when your navigation mixes nav menus with other click-only controls.

Our team Fees
<eun-nav-menu label="Our firm" trigger="click">...</eun-nav-menu>

Placement

placement sets where the panel opens: bottom-start (default), bottom, bottom-end, top-start, top or top-end. It flips to the other side on its own when there isn't enough room, and never overflows the edges of the screen.

Our team Fees Our team Fees Our team Fees Our team Fees
<eun-nav-menu label="Our firm" placement="bottom-end">...</eun-nav-menu>

Following the state

open reflects whether the panel is open, and eunchange fires each time an interaction opens or closes it. Setting open opens or closes it from your own code, without firing the event.

Our team Fees Closed
const menu = document.querySelector("eun-nav-menu");
menu.addEventListener("eunchange", () => {
  console.log(menu.open);
});

In a navigation

In a toolbar

A nav menu sits among the toolbar's other links with the same colors and spacing. Put it in the same slot as them.

Home Our team Fees The office Contact Book a call
<eun-toolbar>
  <a slot="start" href="/">...</a>
  <eun-link slot="center" href="/">Home</eun-link>
  <eun-nav-menu slot="center" label="Our firm">
    <eun-link href="/firm/team">Our team</eun-link>
    <eun-link href="/firm/fees">Fees</eun-link>
    <eun-link href="/firm/office">The office</eun-link>
  </eun-nav-menu>
  <eun-link slot="center" href="/contact">Contact</eun-link>
</eun-toolbar>

The panel opens above everything else on the page, so the toolbar's blur, shadow or rounded corners never cut it off.

On a dark background

theme="dark" switches the trigger and the panel to light text on a dark surface, matching eun-link's own theme="dark". With a toolbar's adaptive-theme, set it from the toolbar's eunchange event.

Home Our team Fees Contact
<eun-nav-menu label="Our firm" theme="dark">...</eun-nav-menu>

Mobile menu

A floating panel doesn't suit a phone. In the toolbar's mobile menu, list the same links in an eun-collapse instead, which expands in place.

Our team Fees The office
<eun-toolbar>
  <!-- Desktop bar -->
  <eun-nav-menu slot="center" label="Our firm">...</eun-nav-menu>

  <!-- Mobile menu, with no slot -->
  <eun-collapse heading="Our firm" chevron-icon="keyboard_arrow_down">
    <eun-link href="/firm/team" block>Our team</eun-link>
    <eun-link href="/firm/fees" block>Fees</eun-link>
    <eun-link href="/firm/office" block>The office</eun-link>
  </eun-collapse>
</eun-toolbar>

Integration

SPA navigation

The links are your own, rendered in the light DOM: a router's link component, such as Next's <Link>, React Router's <NavLink>, Vue's <router-link> or Angular's routerLink, handles the click itself, with client-side navigation and prefetching. eun-link with href raises eunnavigate like anywhere else (see the SPA navigation guide). Following a link closes the panel, so it never stays open over the next page, and a Ctrl, Cmd or middle click opens the page in a new tab without closing it.

Server-side rendering

Before the menu upgrades, its links would show as a plain row of links next to the label. Link overlay-ssr.css in the page's <head> to keep them out of sight, and nav-menu-ssr.css to draw the trigger at its final size, chevron room included, from the first paint:

<link
  rel="stylesheet"
  href="node_modules/@eunomia/elements/dist/ssr/overlay-ssr.css"
/>
<link
  rel="stylesheet"
  href="node_modules/@eunomia/elements/dist/ssr/nav-menu-ssr.css"
/>

A bundler that resolves package exports can reach them as @eunomia/elements/overlay-ssr.css and @eunomia/elements/nav-menu-ssr.css.

Put the trigger's text in the label slot rather than the label attribute, so it's in the server HTML. The links stay in the HTML too, so search engines follow them even while they're hidden.

Custom

Colors and spacing

Override the --nav-menu-* CSS variables, listed in full in the API tab. The --nav-menu-link-* ones style the links of the panel, and an eun-link's own --link-* variables still win over them.

Our team Fees The office
.firm-menu {
  --nav-menu-trigger-color: #831843;
  --nav-menu-trigger-color-hover: #be185d;
  --nav-menu-trigger-background-hover: #fce7f3;
  --nav-menu-trigger-shadow-hover: 0 2px 8px rgba(190, 24, 93, 0.25);
  --nav-menu-trigger-padding: 8px 12px;
  --nav-menu-panel-background: #fdf2f8;
  --nav-menu-panel-border-color: #fbcfe8;
  --nav-menu-panel-padding: 8px;
  --nav-menu-panel-gap: 4px;
  --nav-menu-link-color: #831843;
  --nav-menu-link-color-hover: #be185d;
  --nav-menu-link-background-hover: #fce7f3;
  --nav-menu-offset: 8px;
}

Parts

The trigger and panel parts reach what the variables don't cover, such as a bar under the trigger of the current section.

Our team Fees
eun-nav-menu::part(trigger) {
  position: relative;
}
eun-nav-menu[current]::part(trigger)::after {
  content: "";
  position: absolute;
  inset-inline: 4px;
  bottom: -2px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
}

The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.

Variable Sets
--nav-menu-trigger-color Sets the text and icon color of the trigger
--nav-menu-trigger-color-hover Sets the text and icon color of the trigger on hover, focus and while open
--nav-menu-trigger-color-current Sets the text and icon color of the trigger when it's the current section
--nav-menu-trigger-background-hover Sets the background of the trigger on hover, focus and while open
--nav-menu-trigger-shadow Sets the box shadow of the trigger
--nav-menu-trigger-shadow-hover Sets the box shadow of the trigger on hover, focus and while open
--nav-menu-focus-outline-color Sets the color of the focus ring
--nav-menu-panel-background Sets the background of the panel
--nav-menu-panel-border-color Sets the border color of the panel
--nav-menu-panel-border-radius Sets the corner radius of the panel
--nav-menu-panel-shadow Sets the box shadow of the panel
--nav-menu-link-color Sets the color of the links in the panel
--nav-menu-link-background-hover Sets the background of the links in the panel on hover and focus

Keyboard interactions

Key Action
Tab / Shift+Tab Moves to the trigger, then through the links of the open panel, in order
Enter / Space on the trigger Opens or closes the panel, keeping focus on the trigger
ArrowDown on the trigger Opens the panel and focuses its first link
ArrowUp on the trigger Opens the panel and focuses its last link
ArrowDown / ArrowUp on a link Moves to the next or previous link, wrapping at both ends
Home / End on a link Moves to the first or last link
Enter on a link Follows it, and closes the panel
Escape Closes the panel and returns focus to the trigger, without closing an enclosing dialog

Tabbing past the last link, or anywhere else out of the menu, closes the panel. The arrows are a shortcut: Tab alone reaches every link.

Semantics and screen readers

Disclosure, not menu

eun-nav-menu follows the disclosure navigation pattern of the WAI-ARIA APG, the one recommended for a site's navigation, rather than the menu pattern eun-dropdown-button uses for actions. A role="menu" would switch screen readers to an application mode, expect arrow keys only, and announce "menu items" instead of links.

  • The trigger is a native <button> with aria-expanded and aria-controls pointing at the panel. It never navigates on its own.
  • The panel is a list labelled by the trigger, with one item per link, so a screen reader announces "Our firm, list, 3 items".
  • Every entry stays a real link, announced as a link, with its own destination.
  • The closed panel is out of both the tab order and the accessibility tree.

The current page

The current page's link carries aria-current="page", set by eun-link's current or written on the anchor yourself. The trigger then carries aria-current="true", so the section is announced as current from the trigger too, before the panel is even open.

Label accessibility

The trigger's accessible name is its label. An icon-only trigger has none, since eun-icon renders with aria-hidden="true": set aria-label on eun-nav-menu, which forwards it to the trigger button. The chevron is hidden from assistive technology.

Pointer and touch

Content on hover

The panel opened by hover meets WCAG 1.4.13 Content on Hover or Focus:

  • Dismissible: Escape closes it without moving the pointer.
  • Hoverable: the pointer can move from the trigger into the panel, thanks to hide-delay, without it closing.
  • Persistent: it stays open until the pointer leaves both the trigger and the panel, focus leaves the menu, or it's dismissed.

Touch screens

A touch screen has no hover, so a tap on the trigger opens the panel and another one closes it, whatever trigger is set to. A tap anywhere else on the page closes it too.

Target size

The trigger keeps a padding around its label, which keeps it above the 24 by 24 pixels of WCAG 2.5.8 Target Size (Minimum) at the default font sizes. Keep that in mind before reducing --nav-menu-trigger-padding.

Visual and motion

Focus

The trigger and every link show a visible focus ring from the keyboard, never after a mouse click, colored with --nav-menu-focus-outline-color.

Color contrast

The default colors come from the theme's tokens, already chosen to meet WCAG 1.4.3 Contrast (Minimum), in both themes. A custom --nav-menu-trigger-color must be checked against the background the trigger sits on, and a custom --nav-menu-link-color against --nav-menu-panel-background. See Color for the tokens that already meet it.

Reduced motion

Respects prefers-reduced-motion. The panel's fade and slide, the chevron's rotation and the color transitions are skipped entirely for visitors who asked for reduced motion, satisfying WCAG 2.3.3 Animation from Interactions.

Reference links

WAI-ARIA APG: Disclosure Navigation Menu
WAI-ARIA APG: Disclosure Pattern
WAI Web Accessibility Tutorials: Fly-out Menus
WCAG 1.4.13: Content on Hover or Focus
WCAG 4.1.2: Name, Role, Value
WCAG 2.4.3: Focus Order
WCAG 2.5.8: Target Size (Minimum)
WCAG 2.3.3: Animation from Interactions