Menu, Dropdown, and Context Menu
Menu, Dropdown, and Context Menu: anatomy, keyboard behavior, state, and theming in Askr.
Example
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, Dropdown, DropdownContent, DropdownItem, DropdownTrigger } from '@askrjs/themes/components';
<>
<Dropdown>
<DropdownTrigger aria-label="Project actions">Actions</DropdownTrigger>
<DropdownContent><DropdownItem onSelect={archive}>Archive</DropdownItem></DropdownContent>
</Dropdown>
<ContextMenu>
<ContextMenuTrigger><ProjectCard /></ContextMenuTrigger>
<ContextMenuContent><ContextMenuItem onSelect={duplicate}>Duplicate</ContextMenuItem></ContextMenuContent>
</ContextMenu>
</>Open actions and select an item.
Published props
Generated from the TypeScript declarations shipped by the installed package. Named types in the Type column define the accepted values.
MenuContentAsChildProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
MenuContentProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
MenuGroupAsChildProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
MenuGroupProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
MenuItemAsChildProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onSelect | onSelect?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
textValue | textValue?: string | undefined; | — | Text used for typeahead when rendered children are not plain text. |
type | type?: undefined; | — | — |
MenuItemOwnProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onSelect | onSelect?: ((event: PressEvent) => void) | undefined; | — | — |
textValue | textValue?: string | undefined; | — | Text used for typeahead when rendered children are not plain text. |
MenuItemPartAsChildProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
MenuItemPartProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLSpanElement>; | — | — |
MenuItemProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onSelect | onSelect?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLButtonElement>; | — | — |
textValue | textValue?: string | undefined; | — | Text used for typeahead when rendered children are not plain text. |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
MenuLabelAsChildProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
MenuLabelProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
MenuOwnProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | Supports literal, nested, array-mapped, and `For`-rendered descendants. |
id | id?: string | undefined; | — | — |
loop | loop?: boolean | undefined; | — | — |
orientation | orientation?: Orientation | undefined; | — | — |
MenuProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | Supports literal, nested, array-mapped, and `For`-rendered descendants. |
id | id?: string | undefined; | — | — |
loop | loop?: boolean | undefined; | — | — |
orientation | orientation?: Orientation | undefined; | — | — |
MenuSeparatorAsChildProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
MenuSeparatorProps
Import from @askrjs/ui/menu.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
DropdownContentAsChildProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
align | align?: OverlayAlign | undefined; | — | — |
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
side | side?: OverlaySide | undefined; | — | — |
sideOffset | sideOffset?: number | undefined; | — | — |
DropdownContentOwnProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
align | align?: OverlayAlign | undefined; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
side | side?: OverlaySide | undefined; | — | — |
sideOffset | sideOffset?: number | undefined; | — | — |
DropdownContentProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
align | align?: OverlayAlign | undefined; | — | — |
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
side | side?: OverlaySide | undefined; | — | — |
sideOffset | sideOffset?: number | undefined; | — | — |
DropdownGroupAsChildProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
DropdownGroupProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
DropdownItemAsChildProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onSelect | onSelect?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
textValue | textValue?: string | undefined; | — | Text used for typeahead when rendered children are not plain text. |
type | type?: undefined; | — | — |
value | value?: string | undefined; | — | — |
variant | variant?: DropdownItemVariant | undefined; | — | — |
DropdownItemOwnProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onSelect | onSelect?: ((event: PressEvent) => void) | undefined; | — | — |
textValue | textValue?: string | undefined; | — | Text used for typeahead when rendered children are not plain text. |
value | value?: string | undefined; | — | — |
variant | variant?: DropdownItemVariant | undefined; | — | — |
DropdownItemProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onSelect | onSelect?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLButtonElement>; | — | — |
textValue | textValue?: string | undefined; | — | Text used for typeahead when rendered children are not plain text. |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
variant | variant?: DropdownItemVariant | undefined; | — | — |
DropdownLabelAsChildProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
DropdownLabelProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
DropdownOwnProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | Supports literal, nested, array-mapped, and `For`-rendered descendants. |
defaultOpen | defaultOpen?: boolean | undefined; | — | — |
id | id?: string | undefined; | — | — |
onOpenChange | onOpenChange?: ((open: boolean) => void) | undefined; | — | — |
open | open?: boolean | undefined; | — | — |
DropdownPortalProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
DropdownProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | Supports literal, nested, array-mapped, and `For`-rendered descendants. |
defaultOpen | defaultOpen?: boolean | undefined; | — | — |
id | id?: string | undefined; | — | — |
onOpenChange | onOpenChange?: ((open: boolean) => void) | undefined; | — | — |
open | open?: boolean | undefined; | — | — |
DropdownSeparatorAsChildProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
DropdownSeparatorProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
DropdownTriggerAsChildProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
size | size?: DropdownTriggerSize | undefined; | — | — |
type | type?: undefined; | — | — |
variant | variant?: DropdownTriggerVariant | undefined; | — | — |
DropdownTriggerOwnProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
size | size?: DropdownTriggerSize | undefined; | — | — |
variant | variant?: DropdownTriggerVariant | undefined; | — | — |
DropdownTriggerProps
Import from @askrjs/ui/dropdown.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLButtonElement>; | — | — |
size | size?: DropdownTriggerSize | undefined; | — | — |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
variant | variant?: DropdownTriggerVariant | undefined; | — | — |
Purpose
This page covers two distinct headless primitives that get themed into three components. `Menu` (`@askrjs/ui/menu`) is a plain, always-rendered list of actions with roving keyboard navigation — think the list inside a Menubar. `Dropdown` (`@askrjs/ui/dropdown`) adds the open/close, trigger, and positioned-portal behavior on top, and is what the themed `DropdownMenu` and `ContextMenu` are both built from.
Install and import
Import `Menu`, `MenuItem`, `MenuGroup`, and friends from `@askrjs/ui/menu`; import `Dropdown`, `DropdownTrigger`, `DropdownContent`, and `DropdownItem` from `@askrjs/ui/dropdown`. For styled output, `@askrjs/themes/dropdown-menu` and `@askrjs/themes/context-menu` both resolve to the same themed `Dropdown` implementation under different export names — there's no separate `ContextMenu` component in the themes package, just a relabeled `Dropdown`.
Live examples
A dropdown menu example is a `Dropdown` with a `DropdownTrigger` button and a `DropdownContent` holding one or more `DropdownItem`s, optionally grouped with `DropdownGroup` and `DropdownLabel`. Because `ContextMenu` is the same component, the only real difference in a working example is what you wire to `onOpenChange` — a click for a dropdown, a `contextmenu` event handler you write yourself for a right-click menu.
Anatomy
`Dropdown` is the open-state root; `DropdownTrigger` opens it and `DropdownPortal` carries `DropdownContent` outside the normal DOM flow. Inside, `DropdownItem` is the selectable row, `DropdownGroup` clusters related items, `DropdownLabel` gives a group a heading, and `DropdownSeparator` divides sections. `Menu` mirrors this with `MenuItem`, `MenuGroup`, and `MenuSeparator`, but without a trigger or portal — it's meant to be embedded directly, not popped open.
State model
`Dropdown` takes `open`, `defaultOpen`, and `onOpenChange`, same shape as Tooltip and Dialog — control it yourself if the open state needs to react to something outside the trigger, like a right-click position. `Menu` has no open state at all; its only state-shaped props are `orientation` and `loop`, which govern how arrow-key navigation wraps between items rather than whether the menu is visible.
Keyboard and accessibility
Both primitives use `role="menu"` on their content and `role="menuitem"` on each row, matching the WAI-ARIA menu pattern; `Dropdown`'s trigger additionally carries `aria-expanded`, `aria-controls`, and `aria-haspopup` so assistive tech knows it opens a menu. `Menu` exposes `aria-orientation` for its roving-tabindex arrow navigation, and disabled items on either primitive get `data-disabled` so they're skipped in that navigation rather than just visually dimmed.
Styling and tokens
`DropdownContent` carries `data-state`, `data-side`, and `data-align` for open/close transitions and directional positioning, the same pattern as Tooltip and Popover content. `DropdownItem` supports a `variant` of `'default'` or `'destructive'`, which the default theme uses to color delete-style actions distinctly — since `ContextMenu` shares this CSS wholesale, styling one styles the other.
API
`DropdownContent` accepts `forceMount`, `side`, `align`, and `sideOffset`; `DropdownTrigger` additionally takes `variant` (`'default' | 'ghost'`) and `size` (`'md' | 'icon'`) to match common trigger button shapes. `DropdownItem`'s `onSelect` fires on activation and its `variant` prop is `'default' | 'destructive'`; `MenuItem` has the same `onSelect` shape but no variant, since Menu doesn't own theming concerns.
Edge cases
Because `ContextMenu` is literally `Dropdown` under an alias, it has no built-in right-click trigger — you supply the `contextmenu` event listener, call `preventDefault`, and drive `open`/`onOpenChange` yourself, typically positioning the content at the pointer coordinates rather than relative to a trigger element. Watch out for browser context menus firing alongside yours if you forget the `preventDefault` call, and for `DropdownContent` clipping if it isn't mounted through `DropdownPortal`.
Related pages
See Menubar for a themed, always-visible menu bar built on the same `Menu` primitive. See Popover for anchored content that isn't structured as a role="menu" list, and Tooltip for hover-only hints rather than actionable items.