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

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.