Skip to content

Menus

<details class="menu">
<summary class="menu-trigger">Open menu</summary>
<div class="menu-popup" role="menu">
<button class="menu-item" type="button">
<i class="ti ti-pencil" aria-hidden="true"></i>
Edit
</button>
<button class="menu-item" type="button">
<i class="ti ti-copy" aria-hidden="true"></i>
Duplicate
</button>
<hr class="menu-separator" />
<button class="menu-item" type="button">
<i class="ti ti-trash" aria-hidden="true"></i>
Delete
</button>
</div>
</details>
<details class="menu">
<summary class="menu-trigger">Actions</summary>
<div class="menu-popup" role="menu">
<button class="menu-item" type="button">
<i class="ti ti-pencil" aria-hidden="true"></i>
Edit
</button>
<button class="menu-item" type="button" disabled>
<i class="ti ti-copy" aria-hidden="true"></i>
Duplicate
</button>
<a class="menu-item" href="#changelog" aria-disabled="true">
<i class="ti ti-history" aria-hidden="true"></i>
Changelog
</a>
</div>
</details>
<details class="menu">
<summary class="menu-trigger">View</summary>
<div class="menu-popup" role="menu">
<button
class="menu-item"
type="button"
role="menuitemcheckbox"
aria-checked="true"
>
<span class="menu-item-indicator"
><i class="ti ti-check" aria-hidden="true"></i
></span>
Show grid
</button>
<button
class="menu-item"
type="button"
role="menuitemcheckbox"
aria-checked="false"
>
<span class="menu-item-indicator"
><i class="ti ti-check" aria-hidden="true"></i
></span>
Show ruler
</button>
<hr class="menu-separator" />
<div class="menu-group" role="group">
<div class="menu-group-label">Density</div>
<button
class="menu-item"
type="button"
role="menuitemradio"
aria-checked="true"
>
<span class="menu-item-indicator"
><i class="ti ti-check" aria-hidden="true"></i
></span>
Comfortable
</button>
<button
class="menu-item"
type="button"
role="menuitemradio"
aria-checked="false"
>
<span class="menu-item-indicator"
><i class="ti ti-check" aria-hidden="true"></i
></span>
Compact
</button>
</div>
</div>
</details>
Actions
<Menu>
<Menu.Trigger>Actions</Menu.Trigger>
<Menu.Popup>
<Menu.Item icon={IconPlus} hotkey="mod+n">
New
</Menu.Item>
<Menu.Item icon={IconDeviceFloppy} hotkey="mod+s">
Save
</Menu.Item>
<Menu.Separator />
<Menu.Item icon={IconSearch} hotkey="/">
Search
</Menu.Item>
</Menu.Popup>
</Menu>
<details class="menu">
<summary class="menu-trigger">Resources</summary>
<div class="menu-popup" role="menu">
<div class="menu-group">
<div class="menu-group-label">Internal</div>
<a class="menu-item" href="#docs">
<i class="ti ti-book" aria-hidden="true"></i>
Docs
</a>
<a class="menu-item" href="#changelog">
<i class="ti ti-history" aria-hidden="true"></i>
Changelog
</a>
</div>
<hr class="menu-separator" />
<div class="menu-group">
<div class="menu-group-label">External</div>
<a class="menu-item" href="#support">
<i class="ti ti-lifebuoy" aria-hidden="true"></i>
Support
</a>
</div>
</div>
</details>
<details class="menu">
<summary class="menu-trigger btn btn-primary">Actions</summary>
<div class="menu-popup" role="menu">
<button class="menu-item" type="button">Approve</button>
<button class="menu-item" type="button">Reject</button>
<hr class="menu-separator" />
<button class="menu-item" type="button">Send to review</button>
</div>
</details>
<div class="btn-group">
<button class="btn btn-primary" type="button">Save</button>
<details class="menu">
<summary
class="menu-trigger btn btn-primary btn-square"
aria-label="More save options"
></summary>
<div class="menu-popup" role="menu">
<button class="menu-item" type="button">Save as draft</button>
<button class="menu-item" type="button">Save and publish</button>
<hr class="menu-separator" />
<button class="menu-item" type="button">Save a copy…</button>
</div>
</details>
</div>

A <details> menu has no light dismiss: clicking outside leaves it open. Closing means clicking the trigger again or moving focus away.

Part Renders Class
Menu <details> menu
Menu.Trigger <summary> menu-trigger
Menu.Popup <div> menu-popup
Menu.Item <button>, or <a> with an href menu-item
Menu.Separator <hr> menu-separator
Menu.Group <div> menu-group
Menu.GroupLabel <div> menu-group-label
Part Prop Type Default
Menu.Item icon IconProp
Menu.Item hotkey string | readonly string[]
Menu.Item checked boolean

Open state is the <details> element’s, so there is no open prop to control and no JavaScript involved — pass the native open attribute for a menu that starts expanded. Each part takes the native attributes of its element.

href on an item renders a real <a>, so right-click “open in new tab” and copy-URL work; without it the item is a <button>. Disable a button item with the native disabled attribute and an anchor item with aria-disabled="true", since an <a> has no disabled — both dim, both stop activating, hotkeys included.

checked turns an item into a checkable one and renders the leading check; checked={false} still reserves the gutter, so labels stay aligned down the group. Set role="menuitemradio" for single-select. You hold the state itself.

hotkey dispatches a click on the item and right-pins a Kbd chip, on both the button and anchor branches — see Conventions › Hotkeys.

Class Effect
menu Positioning context on a <details>
menu-trigger <summary> behaviour: pointer cursor, no marker, trailing chevron that rotates when open
menu-popup Popup panel: 11rem min-width, 18rem max-height, scrolls, bordered surface with a shadow
menu-item Row: full width, 0.75rem/0.375rem padding, text-sm, hover tint, 0.5rem gap for an icon
menu-item-indicator 1rem check gutter, always reserved; its glyph shows only when aria-checked="true"
menu-separator 1px divider with 0.25rem of margin
menu-group Column wrapper for a labelled set
menu-group-label Group heading: text-xs uppercase, muted

Open and close is the browser’s, via <details>/<summary> — no JavaScript in either bundle. Write role="menu" on the popup and, for checkable items, role="menuitemcheckbox" or "menuitemradio" plus aria-checked yourself.

menu-trigger only styles behaviour and bows out of appearance when a btn class is present, so adding btn btn-primary to the <summary> gives a button-styled trigger with no override needed. An empty trigger centres its chevron, as the split-button pattern does — pair it with an aria-label, since the chevron is the only visible content. A menu inside a btn-group joins the strip’s rounding and seams.

Where anchor positioning is available the popup is position: fixed and anchored to the trigger, so it escapes an ancestor’s overflow: hidden — inside a <dialog>, for instance — and flips above the trigger when there’s no room below. Without it, the popup falls back to absolute positioning and clips as usual.