Askr documentation
UI & Components

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>
</>
Try Menu

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.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

MenuContentProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

MenuGroupAsChildProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

MenuGroupProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

MenuItemAsChildProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
disableddisabled?: boolean | undefined;
onSelectonSelect?: ((event: PressEvent) => void) | undefined;
refref?: Ref<Element>;
textValuetextValue?: string | undefined;Text used for typeahead when rendered children are not plain text.
typetype?: undefined;

MenuItemOwnProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
childrenchildren?: unknown;
disableddisabled?: boolean | undefined;
onSelectonSelect?: ((event: PressEvent) => void) | undefined;
textValuetextValue?: string | undefined;Text used for typeahead when rendered children are not plain text.

MenuItemPartAsChildProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

MenuItemPartProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLSpanElement>;

MenuItemProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
disableddisabled?: boolean | undefined;
onSelectonSelect?: ((event: PressEvent) => void) | undefined;
refref?: Ref<HTMLButtonElement>;
textValuetextValue?: string | undefined;Text used for typeahead when rendered children are not plain text.
typetype?: "button" | "submit" | "reset" | undefined;

MenuLabelAsChildProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

MenuLabelProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

MenuOwnProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
childrenchildren?: unknown;Supports literal, nested, array-mapped, and `For`-rendered descendants.
idid?: string | undefined;
looploop?: boolean | undefined;
orientationorientation?: Orientation | undefined;

MenuProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
childrenchildren?: unknown;Supports literal, nested, array-mapped, and `For`-rendered descendants.
idid?: string | undefined;
looploop?: boolean | undefined;
orientationorientation?: Orientation | undefined;

MenuSeparatorAsChildProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

MenuSeparatorProps

Import from @askrjs/ui/menu.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

DropdownContentAsChildProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
alignalign?: OverlayAlign | undefined;
asChildasChild: true;
childrenchildren: JSXElement;
forceMountforceMount?: boolean | undefined;
refref?: Ref<Element>;
sideside?: OverlaySide | undefined;
sideOffsetsideOffset?: number | undefined;

DropdownContentOwnProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
alignalign?: OverlayAlign | undefined;
forceMountforceMount?: boolean | undefined;
sideside?: OverlaySide | undefined;
sideOffsetsideOffset?: number | undefined;

DropdownContentProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
alignalign?: OverlayAlign | undefined;
asChildasChild?: false | undefined;
childrenchildren?: unknown;
forceMountforceMount?: boolean | undefined;
refref?: Ref<HTMLDivElement>;
sideside?: OverlaySide | undefined;
sideOffsetsideOffset?: number | undefined;

DropdownGroupAsChildProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

DropdownGroupProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

DropdownItemAsChildProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
disableddisabled?: boolean | undefined;
onSelectonSelect?: ((event: PressEvent) => void) | undefined;
refref?: Ref<Element>;
textValuetextValue?: string | undefined;Text used for typeahead when rendered children are not plain text.
typetype?: undefined;
valuevalue?: string | undefined;
variantvariant?: DropdownItemVariant | undefined;

DropdownItemOwnProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
childrenchildren?: unknown;
disableddisabled?: boolean | undefined;
onSelectonSelect?: ((event: PressEvent) => void) | undefined;
textValuetextValue?: string | undefined;Text used for typeahead when rendered children are not plain text.
valuevalue?: string | undefined;
variantvariant?: DropdownItemVariant | undefined;

DropdownItemProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
disableddisabled?: boolean | undefined;
onSelectonSelect?: ((event: PressEvent) => void) | undefined;
refref?: Ref<HTMLButtonElement>;
textValuetextValue?: string | undefined;Text used for typeahead when rendered children are not plain text.
typetype?: "button" | "submit" | "reset" | undefined;
variantvariant?: DropdownItemVariant | undefined;

DropdownLabelAsChildProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

DropdownLabelProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

DropdownOwnProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
childrenchildren?: unknown;Supports literal, nested, array-mapped, and `For`-rendered descendants.
defaultOpendefaultOpen?: boolean | undefined;
idid?: string | undefined;
onOpenChangeonOpenChange?: ((open: boolean) => void) | undefined;
openopen?: boolean | undefined;

DropdownPortalProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
childrenchildren?: unknown;

DropdownProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
childrenchildren?: unknown;Supports literal, nested, array-mapped, and `For`-rendered descendants.
defaultOpendefaultOpen?: boolean | undefined;
idid?: string | undefined;
onOpenChangeonOpenChange?: ((open: boolean) => void) | undefined;
openopen?: boolean | undefined;

DropdownSeparatorAsChildProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

DropdownSeparatorProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

DropdownTriggerAsChildProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
disableddisabled?: boolean | undefined;
onPressonPress?: ((event: PressEvent) => void) | undefined;
refref?: Ref<Element>;
sizesize?: DropdownTriggerSize | undefined;
typetype?: undefined;
variantvariant?: DropdownTriggerVariant | undefined;

DropdownTriggerOwnProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
sizesize?: DropdownTriggerSize | undefined;
variantvariant?: DropdownTriggerVariant | undefined;

DropdownTriggerProps

Import from @askrjs/ui/dropdown.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
disableddisabled?: boolean | undefined;
onPressonPress?: ((event: PressEvent) => void) | undefined;
refref?: Ref<HTMLButtonElement>;
sizesize?: DropdownTriggerSize | undefined;
typetype?: "button" | "submit" | "reset" | undefined;
variantvariant?: 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`.

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.