When to Use
Use DropdownMenu when a visible button opens a short set of commands or menu
choices. Use ContextMenu when actions belong to a right-clicked target, Select
when one value is chosen for a form, and Menubar for persistent application
commands. Do not use a menu as ordinary site navigation when native links in a
NavList are sufficient.
Submenus use an explicit cascade that is operable by tap/click and keyboard.
Atom does not infer a drill-in presentation from screen size or pointer type.
Applications that need a mobile drill-in flow should compose a separate panel,
Dialog, Drawer, or grouped list with explicit depth and back controls.
Features
The behavior Atom owns before your product adds appearance.
- Supports controlled state, modal behavior, looping, and dismissal options.
- Opens from pointer or keyboard with real focus on the expected first/last item.
- Includes actions, checkbox/radio choices, groups, separators, and submenus.
- Provides typeahead, focus restoration, collision-aware positioning, and RTL submenus.
- Registers portalled content with parent modal focus scopes.
Import
Anatomy
API Reference
Root
Owns shared menu state and renders no wrapper.
open | boolean | - |
defaultOpen | boolean | false |
onOpenChange | (open: boolean) => void | - |
modal | boolean | true |
closeOnSelect | boolean | true |
loop | boolean | true |
closeOnEscape | boolean | true |
Trigger
Renders a native button by default. Click/tap, Enter, Space, and ArrowDown open
with focus on the first item; ArrowUp opens with focus on the last.
disabled | boolean | false |
asChild | boolean | false |
render | RenderProp | - |
role | "button" for custom elements |
aria-haspopup | "menu" |
aria-expanded | Open state |
aria-controls | Generated Content ID |
aria-disabled | "true" when disabled |
[data-slot] | "dropdown-menu-trigger" |
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
Content
Renders the portalled vertical menu, positions it against Trigger, manages real
item focus/typeahead, and applies reason-aware final focus.
The resolved explicit, Trigger, or provider direction is preserved on the
portalled Content and nested SubContent DOM.
side | "top" | "right" | "bottom" | "left" | "bottom" |
align | "start" | "center" | "end" | "start" |
sideOffset | number | 4 |
loop | boolean | Root loop |
ariaLabel | string | - |
onKeyDownCapture | KeyboardEventHandler | - |
asChild | boolean | false |
render | RenderProp | - |
role | "menu" |
aria-orientation | "vertical" |
aria-label | Value from ariaLabel |
aria-labelledby | Trigger ID when ariaLabel is absent |
[data-slot] | "menu-content" |
[data-state] | "open" | "closed" |
[data-side] | Resolved side |
[data-align] | Resolved alignment |
[data-positioned] | Present after positioning |
Group
Renders a semantic group for related entries.
Item
Represents one command. textValue supplies typeahead text when children are
not a plain string.
value | string | required |
textValue | string | Text child or value |
onSelect | () => void | - |
disabled | boolean | false |
closeOnSelect | boolean | Root setting |
asChild | boolean | false |
render | RenderProp | - |
role | "menuitem" |
aria-disabled | "true" when disabled |
[data-slot] | "menu-item" |
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
[data-value] | Item value |
CheckboxItem
Represents an independent menu choice and stays open by default.
value | string | required |
textValue | string | Text child or value |
checked | boolean | "indeterminate" | false |
onCheckedChange | (checked: boolean) => void | - |
disabled | boolean | false |
closeOnSelect | boolean | false |
role | "menuitemcheckbox" |
aria-checked | Checked state |
aria-disabled | "true" when disabled |
[data-slot] | "menu-checkbox-item" |
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
[data-checked] | Present when checked |
[data-value] | Item value |
RadioGroup
Provides one controlled value to nested RadioItems.
value | string | - |
onValueChange | (value: string) => void | - |
[data-slot] | "menu-radio-group" |
RadioItem
Represents one mutually exclusive choice and stays open by default.
value | string | required |
textValue | string | Text child or value |
disabled | boolean | false |
closeOnSelect | boolean | false |
role | "menuitemradio" |
aria-checked | Whether its value matches RadioGroup |
aria-disabled | "true" when disabled |
[data-slot] | "menu-radio-item" |
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
[data-checked] | Present when selected |
[data-value] | Item value |
Separator
Creates a semantic horizontal boundary between entry groups.
role | "separator" |
aria-orientation | "horizontal" |
[data-slot] | "menu-separator" |
Sub
Owns controlled or uncontrolled state for one nested menu and renders no DOM.
open | boolean | - |
defaultOpen | boolean | false |
onOpenChange | (open: boolean) => void | - |
SubTrigger
Renders a menu item that opens SubContent after intentional mouse movement and
a hover delay, by click, or with the direction-aware submenu key. Merely
positioning a menu beneath a stationary pointer does not open the submenu.
value | string | required |
textValue | string | Text child or value |
disabled | boolean | false |
role | "menuitem" |
aria-haspopup | "menu" |
aria-expanded | Sub open state |
aria-disabled | "true" when disabled |
[data-slot] | "menu-sub-trigger" |
[data-state] | "open" | "closed" |
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
[data-value] | Trigger value |
SubContent
Renders a separately portalled nested menu and mirrors placement and open/close
keys in RTL.
sideOffset | number | 4 |
loop | boolean | true |
ariaLabel | string | - |
role | "menu" |
aria-orientation | "vertical" |
aria-label | Value from ariaLabel |
aria-labelledby | SubTrigger ID when unlabeled |
[data-slot] | "menu-sub-content" |
[data-state] | "open" | "closed" |
[data-side] | Resolved side |
[data-positioned] | Present after positioning |
The entry point also exports shared Menu context hooks for advanced custom
parts. Prefer the namespaced parts for the complete behavior above.
Portal, Arrow, Label, and ItemIndicator use the shared Menu contract.
All retained DOM parts accept refs, native props, asChild, and render.
Content and SubContent expose the --atom-menu-available-*,
--atom-menu-trigger-*, and --atom-menu-transform-origin variables.
Examples
Project Actions
Persistent View Options
Accessibility
DropdownMenu follows the
WAI-ARIA Menu pattern.
Trigger provides the menu popup relationship. Content moves real focus among
all entries; disabled entries are announced but cannot activate. Use visible
Trigger text that describes the menu.
Enter / Space / ArrowDown | Opens and focuses the first entry. |
ArrowUp | Opens and focuses the last entry. |
ArrowDown / ArrowUp | Moves through entries while open. |
Home / End | Moves to the first or last entry. |
| Printable character | Moves by typeahead label. |
ArrowRight | Opens a submenu in LTR; closes it in RTL. |
ArrowLeft | Closes a submenu in LTR; opens it in RTL. |
Enter / Space | Activates the highlighted entry. |
Escape | Closes the topmost submenu or menu. |
Tab / Shift+Tab | Closes and moves after/before Trigger in document order. |
Changelog
Unreleased
0.24.0
- Added public Agent Knowledge for component selection, required composition,
recurring mistakes, and validation.
0.20.6
- Inherited movement-gated submenu hover intent so opening a parent
DropdownMenu cannot also open a submenu that appears beneath a stationary
pointer.
0.20.5
- Inherited resolved direction on portalled Content and SubContent so logical
row anatomy and submenu chevrons mirror with the trigger.
- Inherited block-axis submenu collision fallbacks and narrow-viewport
containment when neither inline side fits.
0.20.3
- Inherited document-only overflow locking so sticky application chrome
remains anchored while a modal DropdownMenu is open.
0.12.0
- Inherited real item focus, disabled-item navigation, owner-aware Tab exit,
modal isolation, deferred touch dismissal, and corrected submenu focus.
- Added Portal, Arrow, Label, ItemIndicator, mixed checkbox state, complete DOM
composition, and positioning variables through the shared Menu engine.
- Click/tap opening now focuses the first item; ArrowUp still opens at the last.
0.3.4
- Inherited corrected modal Menu scroll-lock compensation for documents that
preserve their scrollbar gutter.
0.3.1
- Inherited reliable Menu exit-presence cleanup for closed DropdownMenu and
submenu Content under global motion CSS.
0.2.0
- Inherited fixed Menu part
data-slot pass-through so DropdownMenu Content,
Group, Separator, CheckboxItem, RadioGroup, RadioItem, SubTrigger, and
SubContent can be overridden consistently.
- Inherited fixed submenu keyboard behavior under
Direction.Provider dir="rtl"
so ArrowLeft opens submenus, ArrowRight closes submenus, and submenu
placement mirrors to the left side.
- Inherited the shared Menu typeahead behavior so a single-character search
cycles from the current matching item while multi-character buffers still
match exact prefixes.
- Fixed pointer-open behavior so clicking the trigger opens without
pre-highlighting the first item; keyboard opening still seeds first/last
highlight.
- Fixed pointer reopen behavior so closing presence frames cannot leave a stale
first-item highlight for the next trigger click.
- Inherited fixed submenu Escape handling so nested DropdownMenu submenus close
before the root dropdown or parent Dialog/Modal layer.
- Added shared dismissable layer Escape handling so DropdownMenu closes before
parent overlays when nested inside Dialog, Drawer, Modal, or Popover.
- Registered shared Menu content with parent modal focus scopes so DropdownMenu
can remain a valid focus target inside Dialog, Drawer, and other modal
primitives.
0.1.0