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; | — | — |
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; | — | — |
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 DropdownItems, 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.