# Menu, Dropdown, and Context Menu

> Menu, Dropdown, and Context Menu: anatomy, keyboard behavior, state, and theming in Askr.

Source: [https://askrjs.com/docs/components/menu-dropdown-and-context-menu](https://askrjs.com/docs/components/menu-dropdown-and-context-menu)

Status: stable. Packages: @askrjs/ui/menu, @askrjs/ui/dropdown, @askrjs/themes/dropdown-menu, @askrjs/themes/context-menu.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

```tsx
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>
</>
```

## Published props

Generated from the TypeScript declarations shipped by the installed package.

### `MenuContentAsChildProps`

Import from `@askrjs/ui/menu`.

- `asChild: true;`
- `children: JSXElement;`
- `ref?: Ref<Element>;`

### `MenuContentProps`

Import from `@askrjs/ui/menu`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `ref?: Ref<HTMLDivElement>;`

### `MenuGroupAsChildProps`

Import from `@askrjs/ui/menu`.

- `asChild: true;`
- `children: JSXElement;`
- `ref?: Ref<Element>;`

### `MenuGroupProps`

Import from `@askrjs/ui/menu`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `ref?: Ref<HTMLDivElement>;`

### `MenuItemAsChildProps`

Import from `@askrjs/ui/menu`.

- `asChild: true;`
- `children: JSXElement;`
- `disabled?: boolean | undefined;`
- `onSelect?: ((event: PressEvent) => void) | undefined;`
- `ref?: Ref<Element>;`
- `textValue?: string | undefined;` — Text used for typeahead when rendered children are not plain text.
- `type?: undefined;`

### `MenuItemOwnProps`

Import from `@askrjs/ui/menu`.

- `children?: unknown;`
- `disabled?: boolean | undefined;`
- `onSelect?: ((event: PressEvent) => void) | undefined;`
- `textValue?: string | undefined;` — Text used for typeahead when rendered children are not plain text.

### `MenuItemPartAsChildProps`

Import from `@askrjs/ui/menu`.

- `asChild: true;`
- `children: JSXElement;`
- `ref?: Ref<Element>;`

### `MenuItemPartProps`

Import from `@askrjs/ui/menu`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `ref?: Ref<HTMLSpanElement>;`

### `MenuItemProps`

Import from `@askrjs/ui/menu`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `disabled?: boolean | undefined;`
- `onSelect?: ((event: PressEvent) => void) | undefined;`
- `ref?: Ref<HTMLButtonElement>;`
- `textValue?: string | undefined;` — Text used for typeahead when rendered children are not plain text.
- `type?: "button" | "submit" | "reset" | undefined;`

### `MenuLabelAsChildProps`

Import from `@askrjs/ui/menu`.

- `asChild: true;`
- `children: JSXElement;`
- `ref?: Ref<Element>;`

### `MenuLabelProps`

Import from `@askrjs/ui/menu`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `ref?: Ref<HTMLDivElement>;`

### `MenuOwnProps`

Import from `@askrjs/ui/menu`.

- `children?: unknown;` — Supports literal, nested, array-mapped, and `For`-rendered descendants.
- `id?: string | undefined;`
- `loop?: boolean | undefined;`
- `orientation?: Orientation | undefined;`

### `MenuProps`

Import from `@askrjs/ui/menu`.

- `children?: unknown;` — Supports literal, nested, array-mapped, and `For`-rendered descendants.
- `id?: string | undefined;`
- `loop?: boolean | undefined;`
- `orientation?: Orientation | undefined;`

### `MenuSeparatorAsChildProps`

Import from `@askrjs/ui/menu`.

- `asChild: true;`
- `children: JSXElement;`
- `ref?: Ref<Element>;`

### `MenuSeparatorProps`

Import from `@askrjs/ui/menu`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `ref?: Ref<HTMLDivElement>;`

### `DropdownContentAsChildProps`

Import from `@askrjs/ui/dropdown`.

- `align?: OverlayAlign | undefined;`
- `asChild: true;`
- `children: JSXElement;`
- `forceMount?: boolean | undefined;`
- `ref?: Ref<Element>;`
- `side?: OverlaySide | undefined;`
- `sideOffset?: number | undefined;`

### `DropdownContentOwnProps`

Import from `@askrjs/ui/dropdown`.

- `align?: OverlayAlign | undefined;`
- `forceMount?: boolean | undefined;`
- `side?: OverlaySide | undefined;`
- `sideOffset?: number | undefined;`

### `DropdownContentProps`

Import from `@askrjs/ui/dropdown`.

- `align?: OverlayAlign | undefined;`
- `asChild?: false | undefined;`
- `children?: unknown;`
- `forceMount?: boolean | undefined;`
- `ref?: Ref<HTMLDivElement>;`
- `side?: OverlaySide | undefined;`
- `sideOffset?: number | undefined;`

### `DropdownGroupAsChildProps`

Import from `@askrjs/ui/dropdown`.

- `asChild: true;`
- `children: JSXElement;`
- `ref?: Ref<Element>;`

### `DropdownGroupProps`

Import from `@askrjs/ui/dropdown`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `ref?: Ref<HTMLDivElement>;`

### `DropdownItemAsChildProps`

Import from `@askrjs/ui/dropdown`.

- `asChild: true;`
- `children: JSXElement;`
- `disabled?: boolean | undefined;`
- `onSelect?: ((event: PressEvent) => void) | undefined;`
- `ref?: Ref<Element>;`
- `textValue?: string | undefined;` — Text used for typeahead when rendered children are not plain text.
- `type?: undefined;`
- `value?: string | undefined;`
- `variant?: DropdownItemVariant | undefined;`

### `DropdownItemOwnProps`

Import from `@askrjs/ui/dropdown`.

- `children?: unknown;`
- `disabled?: boolean | undefined;`
- `onSelect?: ((event: PressEvent) => void) | undefined;`
- `textValue?: string | undefined;` — Text used for typeahead when rendered children are not plain text.
- `value?: string | undefined;`
- `variant?: DropdownItemVariant | undefined;`

### `DropdownItemProps`

Import from `@askrjs/ui/dropdown`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `disabled?: boolean | undefined;`
- `onSelect?: ((event: PressEvent) => void) | undefined;`
- `ref?: Ref<HTMLButtonElement>;`
- `textValue?: string | undefined;` — Text used for typeahead when rendered children are not plain text.
- `type?: "button" | "submit" | "reset" | undefined;`
- `variant?: DropdownItemVariant | undefined;`

### `DropdownLabelAsChildProps`

Import from `@askrjs/ui/dropdown`.

- `asChild: true;`
- `children: JSXElement;`
- `ref?: Ref<Element>;`

### `DropdownLabelProps`

Import from `@askrjs/ui/dropdown`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `ref?: Ref<HTMLDivElement>;`

### `DropdownOwnProps`

Import from `@askrjs/ui/dropdown`.

- `children?: unknown;` — Supports literal, nested, array-mapped, and `For`-rendered descendants.
- `defaultOpen?: boolean | undefined;`
- `id?: string | undefined;`
- `onOpenChange?: ((open: boolean) => void) | undefined;`
- `open?: boolean | undefined;`

### `DropdownPortalProps`

Import from `@askrjs/ui/dropdown`.

- `children?: unknown;`

### `DropdownProps`

Import from `@askrjs/ui/dropdown`.

- `children?: unknown;` — Supports literal, nested, array-mapped, and `For`-rendered descendants.
- `defaultOpen?: boolean | undefined;`
- `id?: string | undefined;`
- `onOpenChange?: ((open: boolean) => void) | undefined;`
- `open?: boolean | undefined;`

### `DropdownSeparatorAsChildProps`

Import from `@askrjs/ui/dropdown`.

- `asChild: true;`
- `children: JSXElement;`
- `ref?: Ref<Element>;`

### `DropdownSeparatorProps`

Import from `@askrjs/ui/dropdown`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `ref?: Ref<HTMLDivElement>;`

### `DropdownTriggerAsChildProps`

Import from `@askrjs/ui/dropdown`.

- `asChild: true;`
- `children: JSXElement;`
- `disabled?: boolean | undefined;`
- `onPress?: ((event: PressEvent) => void) | undefined;`
- `ref?: Ref<Element>;`
- `size?: DropdownTriggerSize | undefined;`
- `type?: undefined;`
- `variant?: DropdownTriggerVariant | undefined;`

### `DropdownTriggerOwnProps`

Import from `@askrjs/ui/dropdown`.

- `size?: DropdownTriggerSize | undefined;`
- `variant?: DropdownTriggerVariant | undefined;`

### `DropdownTriggerProps`

Import from `@askrjs/ui/dropdown`.

- `asChild?: false | undefined;`
- `children?: unknown;`
- `disabled?: boolean | undefined;`
- `onPress?: ((event: PressEvent) => void) | undefined;`
- `ref?: Ref<HTMLButtonElement>;`
- `size?: DropdownTriggerSize | undefined;`
- `type?: "button" | "submit" | "reset" | undefined;`
- `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.

## Documentation navigation

[Previous](https://askrjs.com/docs/components/tooltip/index.md) | [Next](https://askrjs.com/docs/components/drawer-and-sheet/index.md)
