AntaMenu
Switch to dark theme
Search documentation
On this page

Menu Documentation has not yet been reviewed by a human.

A dropdown or context menu that anchors to a target. Place <Menu> immediately after the trigger — a button, an icon, a whole region — and it opens on click, positions itself with room-aware flipping, and dismisses on outside-click, Escape, or scroll. Rows are dedicated MenuItems (not buttons), with optional leading icons, trailing keyboard hints, separators, grouped sections, and nested submenus.

The trigger can be replaced while Menu stays mounted, such as when a responsive layout switches controls. Keep Menu immediately after the replacement element. Menu reconnects its trigger listeners and popup relationships without resetting its open state.

Open state is uncontrolled by default — pass onStateChange (event-first: (event, { next, prev }), or listen for the raw statechange event) to observe it, or pass open to control it, applying detail.next to open from your onStateChange handler (submenus stay uncontrolled). statechange is cancelable and fires before the menu moves, so uncontrolled you can keep it open with event.preventDefault(). You can also keep a ref and call .open() / .close() / .toggle().

Playground
Render <Menu> right after the trigger element. A MenuItem takes a leading icon, a label (or children), and a trailing kbd hint. Selecting an item closes the menu and fires its onSelect(event, { value, label }); Escape or an outside click dismisses without selecting. Give items a value to route a single handler — onSelect reports the chosen item’s value and label, and never fires for a submenu parent (that opens the flyout) or a selection bubbling up from a nested submenu.
ActionsEditDuplicateExportDelete
// One handler, keyed by each item's `value`.
const onSelect = (e, { value }) => act(value)
<Button>Actions</Button>
<Menu>
<MenuItem icon="edit" label="Edit" kbd="⌘E" value="edit" onSelect={onSelect} />
<MenuItem icon="copy" label="Duplicate" kbd="⌘D" value="duplicate" onSelect={onSelect} />
<MenuItem icon="download" label="Export" value="export" onSelect={onSelect} />
<MenuSeparator />
<MenuItem tone="critical" icon="trash" label="Delete" kbd="⌘⌫" value="delete" onSelect={onSelect} />
</Menu>

Tone

tone colors an item’s label, icon, and hover/selected tint (and its selectionIndicator) with a semantic palette — brand, info, success, warning, or critical — tracking light and dark mode automatically. Omit it (or pass neutral) for the standard gray. critical is the conventional destructive action. Pass any CSS color for a one-off custom tone — its hue and chroma are kept while the lightness is pinned to match the brand text (resolved through --menu-item-tone-source).

toneScope="selected" applies tone only while the row is selected — an unselected row stays neutral. Select uses the same pair to tone chosen rows.

StatusNeutralBrandInfoSuccessWarningCritical
<Menu>
<MenuItem label="Neutral" />
<MenuItem tone="brand" icon="star" label="Brand" />
<MenuItem tone="info" icon="info" label="Info" />
<MenuItem tone="success" icon="circle-check" label="Success" />
<MenuItem tone="warning" icon="warning-triangle" label="Warning" />
<MenuItem tone="critical" icon="trash" label="Critical" />
</Menu>

Placement

placement sets the preferred side and edge alignment: bottom-start (the default), bottom-end, top-start, or top-end. The -start / -end suffix aligns the menu’s left or right edge to the trigger. The menu auto-flips vertically and clamps horizontally when there isn’t room, so the placement is a preference, not a constraint.
bottom-startOneTwobottom-endOneTwotop-startOneTwotop-endOneTwo
<Button>bottom-start</Button>
<Menu placement="bottom-start">…</Menu>
<Button>bottom-end</Button>
<Menu placement="bottom-end">…</Menu>
<Button>top-start</Button>
<Menu placement="top-start">…</Menu>
<Button>top-end</Button>
<Menu placement="top-end">…</Menu>

The menu positions against the trigger’s box. A trigger can narrow that: if it implements getAnchorRect(): DOMRect, the menu positions against that rect instead of its full border box. Anta’s Input does this — it reports its field box — so a Menu placed after an <Input> (as in Select) lines up with the field, not the whole component (whose box also spans the label and hint).

Open at the pointer

Pass context to open on right-click (the contextmenu event) of the region the menu follows, positioned at the pointer — ideal for content surfaces like a log viewer or canvas where secondary actions shouldn’t clutter the UI. coord on its own opens a normal left-click menu at the cursor instead of aligned to the trigger box.
Right-click anywhere here
Share logCopy moment
<div class="canvas">Right-click anywhere here</div>
<Menu context>
<MenuItem icon="link" label="Share log" onSelect={share} />
<MenuItem icon="copy" label="Copy moment" onSelect={copy} />
</Menu>

Mark a MenuItem with submenu and nest a <Menu> inside it to create a flyout (the nested menu detects it’s a submenu from being inside the item). The parent item gets a chevron (override it per item with iconTrailing), aria-haspopup="menu", and an aria-expanded that tracks the flyout’s open state; the submenu opens to the side and flips when it nears the edge. Submenus open on hover (with intent timing, so a quick pass-through doesn’t trigger them) as well as on click; add nohover to a submenu to make it click-only. Hover-intent is mouse-only regardless: on touch a submenu opens on tap and stays open until you dismiss it or open a sibling.

For a top-level dropdown, add aria-haspopup="menu" to your own trigger element — it’s a control you render, so the accessible name and popup hint live there.

MoreRenameMove toProjectsArchiveViewColumnsVisible columns

Submenus nest arbitrarily deep — here View → Columns → Visible columns is three flyouts in. The deepest one isn’t MenuItems but custom content: a checkbox list whose region carries data-menu-open, so ticking a box (or the Select all toggle, or a row’s Only button — which leaves just that column on) updates state without closing the menu.

const COLUMNS = ['Name', 'Status', 'Owner', 'Created', 'Modified']
const [on, setOn] = useState(() => Object.fromEntries(COLUMNS.map((c) => [c, true])))
const allOn = COLUMNS.every((c) => on[c])
const someOn = COLUMNS.some((c) => on[c])
const only = (c) => setOn(Object.fromEntries(COLUMNS.map((k) => [k, k === c])))
// …toggle / toggleAll omitted
<Button>More</Button>
<Menu>
<MenuItem icon="edit" label="Rename" />
<MenuItem label="Move to" submenu>
<Menu>
<MenuItem icon="folder-open" label="Projects" />
<MenuItem icon="folder-open" label="Archive" />
</Menu>
</MenuItem>
<MenuItem label="View" submenu>
<Menu>
<MenuItem label="Columns" submenu>
<Menu>
<MenuItem label="Visible columns" submenu>
<Menu>
{/* custom content — never auto-closes thanks to data-menu-open */}
<div class="menu-check-row" data-menu-open>
{/* Checkbox takes 'indeterminate' directly — no ref poking */}
<Checkbox
className="menu-check"
label="Select all"
checked={allOn ? true : someOn ? 'indeterminate' : false}
onStateChange={toggleAll}
/>
</div>
<MenuSeparator />
{COLUMNS.map((c) => (
<div class="menu-check-row" data-menu-open key={c}>
<Checkbox className="menu-check" label={c} checked={on[c]} onStateChange={() => toggle(c)} />
<Button priority="tertiary" size="small" onClick={() => only(c)}>Only</Button>
</div>
))}
</Menu>
</MenuItem>
</Menu>
</MenuItem>
</Menu>
</MenuItem>
</Menu>

Groups

Wrap related items in a MenuGroup with a label to add a titled section. Headings are skipped during keyboard navigation, which flattens all items into a single up/down sequence across group boundaries.

A MenuSeparator is a plain hairline when empty, but given text (<MenuSeparator>No results</MenuSeparator>) it renders a small muted caption and becomes an aria-live="polite" status region — the styled home for a status or empty-state message (what Select’s renderEmpty uses).

ViewNameDate modifiedSizeHidden files
<Menu>
<MenuGroup label="Sort by">
<MenuItem icon="check" label="Name" />
<MenuItem label="Date modified" />
</MenuGroup>
<MenuSeparator />
<MenuGroup label="Show">
<MenuItem label="Hidden files" data-menu-open />
</MenuGroup>
</Menu>

a-menu-header and a-menu-footer name fixed regions around the scrolling items. Put a-select-header or a-select-footer in the same slots when a composed Select needs its own DOM anatomy. A MenuSeparator slotted into the footer stays the menu’s divider.

FilterDesignEngineeringClear filters
<Button>Filter</Button>
<Menu inset={8}>
<a-menu-header slot="header" data-menu-open>
<Input size="small" placeholder="Filter…" aria-label="Filter items" />
</a-menu-header>
<MenuItem label="Design" />
<MenuItem label="Engineering" />
<MenuSeparator slot="footer" />
<a-menu-footer slot="footer">
<MenuItem icon="x" label="Clear filters" data-menu-open />
</a-menu-footer>
</Menu>

Custom content and the close behavior

The close decision is read straight from the DOM on click — never from whether a handler called preventDefault — so it’s safe in runtimes where event handlers run off the UI thread. Walking out from the click, the nearest marker wins:

  • A MenuItem is a choice → selecting it closes the menu.
  • Arbitrary content you inject never closes it — drop a slider, an input, or any control straight into a <Menu> and interacting leaves the menu open.
  • data-menu-open keeps the menu open past a click — put it on an item, a MenuGroup, or any wrapping element (toggles, multi-select).
  • data-menu-close opts arbitrary content into closing — so a custom element (or a “Done” button inside a data-menu-open region) can dismiss the menu without being a MenuItem.

Selecting an item won’t also click the region the menu sits in — the activation click stops at the surface, so a <Menu> inside a clickable row or card is safe. Other events still bubble; pass stopPropagation (true for clicks, or event names like "click pointerdown") to contain those too.

For programmatic open/close from app state, drive the open prop (the state attribute, "open" / "closed") and react via onStateChange, or keep a ref and call .open() / .close() / .toggle().

SettingsProfileNotifications
<Menu>
<MenuItem label="Profile" onSelect={openProfile} />
{/* data-menu-open: a toggle that doesn't dismiss */}
<MenuItem label="Notifications" data-menu-open onSelect={toggleNotifs} />
<MenuSeparator />
{/* data-menu-open region: interacting with the slider keeps the menu open;
the Done button opts back into closing with data-menu-close */}
<div data-menu-open class="zoom">
<label>Zoom</label>
<input type="range" />
{/* a plain button, not a MenuItem — data-menu-close lets it dismiss */}
<button data-menu-close>Done</button>
</div>
</Menu>

Inside a clickable row

A <Menu> can live inside a clickable row or card without its items triggering that row’s onClick — selecting an item is contained at the menu surface. In the row below, the counter moves when you click the row, but not when you pick a menu item. The kebab trigger is a plain button, so its click is stopped by hand; the menu items need nothing. To contain more (a slotted control’s clicks, say), add stopPropagation to the <Menu>.
Quarterly report.pdfClick row: 0RenameDuplicateDelete
<div className="row" onClick={openRow}>
<span>Quarterly report.pdf</span>
{/* the trigger is a plain button — stop its click so opening the
menu isn't a row click; the menu items need no handling */}
<Button
icon="dots-vertical"
aria-label="File actions"
onClick={(e) => e.stopPropagation()}
/>
<Menu>
<MenuItem icon="edit" label="Rename" onSelect={rename} />
<MenuItem icon="copy" label="Duplicate" onSelect={duplicate} />
<MenuSeparator />
<MenuItem icon="trash" tone="critical" label="Delete" onSelect={confirmDelete} />
</Menu>
</div>

Give a MenuItem an href and it renders a native <a> instead of the custom element — a real link. It navigates on click, opens in a new tab on ⌘ / middle-click, offers “copy link address”, and takes the anchor attributes: target, rel, ping, and download (true for the resource’s default filename, a string to rename it). The parent Menu treats it as a first-class row — arrow-key navigation, Enter / Space activation, and close-on-select — mixed freely with regular items. onSelect fires alongside the navigation; disabled drops the href so it can’t navigate; selected marks a current link (aria-current).

Under the hood it’s <a role="menuitem" data-anta-menu-item href>, the same opt-in-marker pattern as Button’s link mode.

ResourcesDocumentationDownload reportPreferences
<Menu>
{/* Real links — open in a new tab, or download */}
<MenuItem icon="file" label="Documentation" href="/docs" target="_blank" iconTrailing="external-link" />
<MenuItem icon="download" label="Download report" href="/report.pdf" download="report.pdf" />
<MenuSeparator />
{/* A regular action row, mixed in */}
<MenuItem icon="settings" label="Preferences" onSelect={openPrefs} />
</Menu>

Selected

selected gives a row a persistent background tint — the resting fill a pressed row shows — marking it as chosen. It touches only the background, so nothing shifts and no gutter is reserved. It’s the building block for select-style menus: mark one row for single-select, several for multi-select (pair with data-menu-open to keep the menu open as choices toggle). The tint tracks tone, so a toned row stays in its own color.

For a checkable row, add selectionIndicator. The checkbox and radio variants render a passive leading mark. The switch variant renders a passive trailing switch for settings that take effect immediately. The row remains the control and carries aria-checked; the indicator adds no focus stop or nested role. A switch row keeps the menu open when selected.

Select’s selection modes use the checkbox and radio variants.

Sort byRelevanceNewestOldestMost viewedRelevanceNewestShow archivedShow drafts
{/* Plain tint (no indicator) */}
<MenuItem label="Relevance" selected />
{/* Checkable rows — the row is the control; the mark is decorative */}
<MenuItem selectionIndicator="radio" label="Relevance" selected />
<MenuItem selectionIndicator="checkbox" label="Show archived" selected data-menu-open />
SettingsNotificationsAutomatic updatesInstall new versions automatically
const [notifications, setNotifications] = useState(true)
const [automaticUpdates, setAutomaticUpdates] = useState(false)
<Button>Settings</Button>
<Menu>
<MenuItem
selectionIndicator="switch"
label="Notifications"
selected={notifications}
onSelect={() => setNotifications((value) => !value)}
/>
<MenuItem
selectionIndicator="switch"
label="Automatic updates"
hint="Install new versions automatically"
selected={automaticUpdates}
onSelect={() => setAutomaticUpdates((value) => !value)}
/>
</Menu>

Badges, counters, and hints

Drop a <Tag> (or any element) as a child of a MenuItem for a trailing counter or badge. Children render after the label but before the kbd hint and trailing icon, and the label’s flex: 1 right-aligns them — so a small tag sits at the row’s edge, just left of a shortcut or submenu chevron. Use size="small" so it sits comfortably in the row.

Add hint for secondary text under the label — muted, and it tracks the row’s tone. It stacks in a column beneath the label while the icon, badge, and chevron stay centered on the row, so it composes with a trailing counter.

FoldersActive12 open items12ArchivedRead-onlyTrash3
<Menu>
<MenuItem icon="folder-open" label="Active" hint="12 open items"><Tag size="small" tone="info">12</Tag></MenuItem>
<MenuItem icon="folder-close" label="Archived" hint="Read-only" />
<MenuItem icon="trash" label="Trash"><Tag size="small">3</Tag></MenuItem>
</Menu>

MenuItemCopy is a row that copies to the clipboard when chosen: copy for a string, copyNode for a DOM region, or copyUrl for the current URL. It composes a <MenuItem> with a slotted <a-copy> element that performs the write, and keeps the menu open on select so the check / retone feedback shows. onCopied(ok) fires after each attempt. iconPlacement="trailing" moves the feedback glyph to the end of the row; "none" shows a cursor-near confirmation instead.

Copy sits alongside normal items, so a “Share” menu can mix a copy-link row with navigating links.

ShareCopy linkCopy embedOpen in new tab
<Button icon="share" iconTrailing="chevron-down" label="Share" />
<Menu>
<MenuItemCopy copy={pageUrl} label="Copy link" kbd="⌘C" />
<MenuItemCopy copy={embedHtml} icon="braces" label="Copy embed" />
<MenuSeparator />
<MenuItem icon="external-link" label="Open in new tab" href="/menu" target="_blank" />
</Menu>

Props

Prop Type Default Description
icon? IconShape — Leading icon shape.
label? ReactNode — The item's text. Usually a string, but any node is accepted — e.g. a filtered Select bolds the matched substring. Omit and pass children for richer content.
hint? ReactNode — Secondary text under the label — explanatory copy, like RadioGroup's option hint. Requires label (it stacks in a column beneath it). Muted (--text-3) and tracks the row's tone. A string, or any node.
kbd? string — Trailing shortcut hint, e.g. "⌘E". Anta translates it to aria-keyshortcuts and hides the visual glyphs from assistive technology. Use + within a chord and spaces between shortcuts ("⌘K ⌘S" becomes "Meta+K Meta+S"). Pass aria-keyshortcuts to override the translation. This does not bind the shortcut.
disabled? boolean — Disable the item: greyed out, not focusable for activation, no close. A disabled link also drops its href, so it can't navigate.
selected? boolean — Mark the item as selected. On a plain row (no selectionIndicator) this is a persistent background tint, the same resting fill a pressed row shows — also the way to flag the current page on a link item. On a checkable row (selectionIndicator set) it instead drives the indicator and the row's aria-checked.
tone? neutralbrandcriticalinfosuccesswarningstring neutral Semantic tone — colors the label, icon, hover/selected tint, and selection indicator. A named tone, or any literal CSS color ('#ff1493', 'rebeccapurple') for a one-off custom tone whose hue + chroma are kept while the lightness is pinned to match the brand text. critical is the destructive action; neutral (the default) is the standard gray.
toneScope? allselected 'all' Apply tone to every row state, or only while the row is selected. In selected scope, an unselected row and its indicator stay neutral.
value? stringnumber — An opaque value identifying this item, handed back in onSelect's detail so a shared handler can tell which row was chosen without a per-item closure.
onSelect? (event, detail) => void — Activation handler — fires when this item is chosen (click / Enter / Space), unless it's disabled. It does not fire for a submenu parent (clicking that opens the flyout, which isn't a selection) nor for a selection bubbling up from a nested submenu. On a link item it fires alongside the navigation. Receives the event plus a { value, label } detail.
onMouseDown? (event) => void — Raw mousedown on the row. Mainly to preventDefault() so the row doesn't take focus on a mouse press — e.g. a combobox option keeping focus in its input field while the click still selects.
role? string — ARIA role override. Defaults to the role implied by selectionIndicator (menuitem / menuitemcheckbox / menuitemradio); set it to reparent the row under a different container role — e.g. option inside a listbox.
children? ReactNode — Item content. With label set, children are extra content — most notably the nested <Menu> for a submenu parent.
copy? string — Text copied to the clipboard on activation.
copyWithUrl? boolean — Prefix the copied text with // URL: <current page URL>.
onCopyRequest? () => void — Refresh a dynamic copy value before activation. Set the new string in application state so the next render updates copy. Return values are ignored. Fires on pointerdown and Enter/Space keydown.
onCopied? (ok) => void — Fires after the copy attempt with whether it succeeded.
copyNode? booleanstring — Copy a DOM node as rich text (text/html) + plain text. true copies the nearest ancestor marked data-copy-source; a string is a CSS selector for an ancestor region (closest). The copy control is stripped from the copied output.
copyUrl? true — Copy the current page URL (location.href).
iconPlacement? leadingtrailingnone 'leading' Where the copy glyph sits relative to the label — or 'none' to omit it. Without a glyph, a successful copy shows a confirmation label near the pointer and leaves the row unchanged.
copiedLabel? string Copied Text in the successful no-icon confirmation.
Inherited props (className, id, slot, style, tabIndex, title)
Prop Type Default Description
className? string — CSS class on the component's root element (merged with the component's own classes). Use it directly for layout and positioning — grid/flex placement, margins, alignment — rather than wrapping the component in a <div>/<span>.
id? string — HTML id attribute.
slot? string — Assigns the element to a named <slot> of a parent web component (e.g. slot="header" inside a <Dialog>, slot="footer" inside a <Dialog>).
style? CSSProperties — Inline styles on the component's root element. Set layout/positioning here (or via className) directly on the component instead of adding a wrapper.
tabIndex? number — Tab order. Set to -1 to skip the element when tabbing.
title? string — HTML title attribute — native browser tooltip on hover.

Plus every standard DOM event handler (onClick, onFocus, onKeyDown, …) and any data-* or aria-* attribute — forwarded as-is to the underlying <a-*> element.

Keyboard

While open, the menu keeps Tab focus contained. Open the trigger with Enter / Space to focus the first item.

Controlled vs Uncontrolled

Open state follows Anta’s shared state contract. By default the menu is uncontrolled — its triggers (click / right-click / .open()) own open/close, so there’s no prop to set. Pass open to control it: visibility follows the prop, and a user dismiss only requests a change. The <Menu> wrapper takes a boolean; the <a-menu> element takes a state string enum:

<Menu> (JSX) <a-menu> (element)
Uncontrolled (default) — triggers own it (no prop) (no state attribute)
Controlled — you own it open + onStateChange state="open" | "closed" + statechange
Change event onStateChange(event, { next, prev }) — next / prev are booleans statechange, a CustomEvent<{ next, prev }> — 'open' / 'closed' (plus coord / originEvent)

statechange is cancelable and fires before the menu moves. Uncontrolled, call event.preventDefault() to keep the menu as-is (e.g. block a dismiss). Controlled, the menu never self-moves — apply detail.next to open to accept, or do nothing to reject. (Submenus are always uncontrolled, regardless of open.)

Prop Type Default Description
placement? bottom-startbottom-endtop-starttop-endbottomtopright-startright-endleft-startleft-endrightleft bottom-start Preferred placement relative to the trigger. bottom / top place the menu below / above; right / left place it beside (like a submenu flyout). The cross-axis suffix (-start / -end) aligns the near / far edges, and no suffix centers on the trigger. The menu flips to the opposite side when the preferred one lacks room, and clamps to the viewport.
context? boolean — Open on right-click (the contextmenu event) of the trigger region instead of a left click, positioned at the pointer.
coord? boolean — Open at the pointer coordinates rather than aligned to the trigger box. Pairs naturally with context; on its own it positions a left-click menu at the cursor.
nohover? boolean — Submenu-only (a <Menu> nested inside a MenuItem); ignored on a root menu. Submenus open on hover by default (with intent timing) as well as on click — set nohover to make this submenu click-only. Hover-intent is mouse-only regardless: on touch (and pen) a submenu always opens on tap and stays open until dismissed.
offset? number 4 Gap in pixels between the trigger and the menu.
autoWidth? boolean — Size the menu to its content instead of flooring its width to the trigger. A root menu is never narrower than its trigger by default; set this for a content menu under a wide trigger (e.g. a calendar below a full-width field) so it wraps its content and left-aligns under the trigger.
inset? numberstring 4 Inner inset for the scroll body and named header/footer content. A number is pixels; a CSS length string is used as written.
open? boolean — Controlled open state. Omit for the default uncontrolled menu (it opens/closes itself via its triggers). Pass a boolean to control it: the menu's visibility follows open, and user dismiss (Esc, outside-click, select) fires onStateChange without self-closing — you update open in response. Submenus are always uncontrolled regardless of this. See STATEFUL-COMPONENTS.md.
round? booleannumberstring — Round the menu container to a 20px radius. A number (px) or CSS length string sets a custom container radius. Items use the container radius minus inset, clamped to zero. Nested submenus inherit the container radius unless they set their own round value.
stopPropagation? booleanstringstring[] — Contain events so they don't bubble out of the menu to ancestor handlers (e.g. a clickable row the menu sits in). Selecting an item is always contained; this extends it to whole event types. true contains click; pass an event name or list ("click pointerdown") to contain those.
onStateChange? (event, detail) => void — Fired before the open state changes — on open, and on every dismiss (Esc, outside-click, scroll, selecting an item). event is the cancelable statechange; detail.next/detail.prev are the requested/previous open state (booleans). It's the declarative way to observe a menu, and the handler you pair with open to drive a controlled menu (apply detail.next to open). Uncontrolled, event.preventDefault() vetoes the transition (e.g. keep the menu open).
onactivedescendant? (e) => void — Combobox-mode cursor report (raw element event). Fires when the active option changes as arrow keys move the cursor while focus stays in a [data-menu-search] filter field; detail.id is an authored option id, or null. Built-in Anta Inputs receive the active option through a direct ARIA element relationship; this remains available to custom fields and observers. Passed straight through to the element.
role? string menu ARIA role of the menu surface.
children? ReactNode — The menu's contents: MenuItem, MenuSeparator, MenuGroup, or any custom element.
Inherited props (className, id, slot, style, tabIndex, title)
Prop Type Default Description
className? string — CSS class on the component's root element (merged with the component's own classes). Use it directly for layout and positioning — grid/flex placement, margins, alignment — rather than wrapping the component in a <div>/<span>.
id? string — HTML id attribute.
slot? string — Assigns the element to a named <slot> of a parent web component (e.g. slot="header" inside a <Dialog>, slot="footer" inside a <Dialog>).
style? CSSProperties — Inline styles on the component's root element. Set layout/positioning here (or via className) directly on the component instead of adding a wrapper.
tabIndex? number — Tab order. Set to -1 to skip the element when tabbing.
title? string — HTML title attribute — native browser tooltip on hover.

Plus every standard DOM event handler (onClick, onFocus, onKeyDown, …) and any data-* or aria-* attribute — forwarded as-is to the underlying <a-*> element.

Prop Type Default Description
icon? IconShape — Leading icon shape.
label? ReactNode — The item's text. Usually a string, but any node is accepted — e.g. a filtered Select bolds the matched substring. Omit and pass children for richer content.
hint? ReactNode — Secondary text under the label — explanatory copy, like RadioGroup's option hint. Requires label (it stacks in a column beneath it). Muted (--text-3) and tracks the row's tone. A string, or any node.
kbd? string — Trailing shortcut hint, e.g. "⌘E". Anta translates it to aria-keyshortcuts and hides the visual glyphs from assistive technology. Use + within a chord and spaces between shortcuts ("⌘K ⌘S" becomes "Meta+K Meta+S"). Pass aria-keyshortcuts to override the translation. This does not bind the shortcut.
iconTrailing? IconShape — A trailing icon. On a submenu item this overrides the default chevron (omit it to keep the chevron); on a normal item it's the trailing glyph (omit for none).
disabled? boolean — Disable the item: greyed out, not focusable for activation, no close. A disabled link also drops its href, so it can't navigate.
selected? boolean — Mark the item as selected. On a plain row (no selectionIndicator) this is a persistent background tint, the same resting fill a pressed row shows — also the way to flag the current page on a link item. On a checkable row (selectionIndicator set) it instead drives the indicator and the row's aria-checked.
tone? neutralbrandcriticalinfosuccesswarningstring neutral Semantic tone — colors the label, icon, hover/selected tint, and selection indicator. A named tone, or any literal CSS color ('#ff1493', 'rebeccapurple') for a one-off custom tone whose hue + chroma are kept while the lightness is pinned to match the brand text. critical is the destructive action; neutral (the default) is the standard gray.
toneScope? allselected 'all' Apply tone to every row state, or only while the row is selected. In selected scope, an unselected row and its indicator stay neutral.
value? stringnumber — An opaque value identifying this item, handed back in onSelect's detail so a shared handler can tell which row was chosen without a per-item closure.
onSelect? (event, detail) => void — Activation handler — fires when this item is chosen (click / Enter / Space), unless it's disabled. It does not fire for a submenu parent (clicking that opens the flyout, which isn't a selection) nor for a selection bubbling up from a nested submenu. On a link item it fires alongside the navigation. Receives the event plus a { value, label } detail.
onMouseDown? (event) => void — Raw mousedown on the row. Mainly to preventDefault() so the row doesn't take focus on a mouse press — e.g. a combobox option keeping focus in its input field while the click still selects.
role? string — ARIA role override. Defaults to the role implied by selectionIndicator (menuitem / menuitemcheckbox / menuitemradio); set it to reparent the row under a different container role — e.g. option inside a listbox.
children? ReactNode — Item content. With label set, children are extra content — most notably the nested <Menu> for a submenu parent.
href? string — Renders the item as <a role="menuitem" data-anta-menu-item href>.
target? string — Anchor target, e.g. '_blank'.
rel? string — Anchor rel.
download? stringboolean — Download the resource instead of navigating: true / '' uses the resource's default filename, a string overrides it.
ping? string — Space-separated URLs the browser pings on navigation.
submenu? boolean — Marks this item as a submenu parent: adds the trailing chevron and aria-haspopup="menu". Nest the flyout as a <Menu> child.
selectionIndicator? checkboxradiocheckswitch — Turn the row into a checkable item, driven by selected (the row stays the control and carries aria-checked): - 'checkbox' → role="menuitemcheckbox", a leading passive <a-checkbox> (before icon); the tint is dropped (the box carries state). - 'radio' → role="menuitemradio", a leading passive <a-radio>; tint dropped. - 'check' → role="menuitemradio", a trailing check glyph on the selected row and the background tint (the canonical single-select look). - 'switch' → role="menuitemcheckbox", a trailing passive <a-switch>; selecting the row keeps its menu open. Omit for a plain row (the default).
indeterminate? boolean — Only meaningful with selectionIndicator="checkbox": render the box in the mixed state (aria-checked="mixed") — e.g. a "Select all" row when some but not all of its options are selected.
indicator? React.ReactNode — Replace the built-in selection-indicator visual with your own node, rendered at the leading edge (where the checkbox / radio sit). Pair with selectionIndicator to keep the semantics — the row stays the control and carries role + aria-checked; only the drawn mark changes. Suppresses the built-in checkbox / radio and the trailing check glyph. The node is made passive (aria-hidden, no pointer events) so the row receives the click.
Inherited props (className, id, slot, style, tabIndex, title)
Prop Type Default Description
className? string — CSS class on the component's root element (merged with the component's own classes). Use it directly for layout and positioning — grid/flex placement, margins, alignment — rather than wrapping the component in a <div>/<span>.
id? string — HTML id attribute.
slot? string — Assigns the element to a named <slot> of a parent web component (e.g. slot="header" inside a <Dialog>, slot="footer" inside a <Dialog>).
style? CSSProperties — Inline styles on the component's root element. Set layout/positioning here (or via className) directly on the component instead of adding a wrapper.
tabIndex? number — Tab order. Set to -1 to skip the element when tabbing.
title? string — HTML title attribute — native browser tooltip on hover.

Plus every standard DOM event handler (onClick, onFocus, onKeyDown, …) and any data-* or aria-* attribute — forwarded as-is to the underlying <a-*> element.

Web Component

Use <a-menu> and <a-menu-item> directly outside JSX.

Put a focusable trigger immediately before <a-menu> and omit state for an uncontrolled menu. Its light-DOM <a-menu-item> rows need their own tabindex values.

Follow Install and configure to load styles and browser behavior. For server-rendered applications, also follow Browser hydration.

ActionsSettingsMembers
<a-button role="button" tabindex="0" aria-haspopup="menu">
<a-button-label>Actions</a-button-label>
</a-button>
<a-menu role="menu">
<a-menu-item role="menuitem" tabindex="0" value="settings"><a-menu-item-label>Settings</a-menu-item-label></a-menu-item>
<a-menu-item role="menuitem" tabindex="0" value="members"><a-menu-item-label>Members</a-menu-item-label></a-menu-item>
</a-menu>

Styling

Menu item corners follow the container radius minus inset, clamped to zero. The default 8px container radius and 4px inset give items a 4px radius. The Antithesis theme uses a 2px container radius, so its items have square corners with the default inset. Pass round for a 20px container radius and 16px item radius, or pass a number or CSS length for a custom container radius. Nested submenus inherit the container radius unless they set their own round value. Each submenu subtracts its own inset to determine its item radius. Use round and inset to keep these radii connected. A direct CSS override of ::part(menu) or a menu item’s border-radius controls that surface independently.

The popover surface lives in shadow DOM. Style its chrome (background, frost, border, radius, shadow, padding, min-width) with ::part(menu). Use ::part(scroll) for the scrolling body and ::part(header) / ::part(footer) for the pinned slots. Keep display overrides scoped to ::part(menu):popover-open so closed menus remain hidden. Position and available height follow the trigger and viewport; overrides of positioning, visibility, transforms, or transitions can change those behaviors.

Put overrides in components, utilities, or unlayered CSS. If older part overrides are in base, move them to a later layer so they take precedence over Anta’s defaults. See Cascade layers.

Menu items are light DOM, so style them (and their a-icon / kbd / a-menu-item-label children) directly. Match both item shapes — the a-menu-item custom element and the a[data-anta-menu-item] link — with :is(a-menu-item, a[data-anta-menu-item]), or a link row skips your item rule. Click the trigger to open the styled menu; the .fancy class is just for the demo:

ActionsEditOpen docsDelete
/* a "liquid glass" menu — more transparent + a stronger blur, big radius, fully
rounded items with roomier padding and bigger icons / type. */
a-menu.fancy::part(menu) {
border-radius: 22px;
min-width: 240px;
padding: 8px;
background: light-dark(color-mix(in oklch, white 55%, transparent), color-mix(in oklch, rgb(28,26,32) 50%, transparent));
backdrop-filter: blur(28px) saturate(180%);
}
/* Items are light DOM — style them directly. `:is()` catches both an
<a-menu-item> and a link row (<a data-anta-menu-item>). */
a-menu.fancy :is(a-menu-item, a[data-anta-menu-item]) {
border-radius: 999px;
padding: 10px 16px;
font-size: 15px;
--icon-size: 18px; /* inherited by the item's <a-icon> */
}