Components

DropdownMenu

Headless button-triggered menu primitives for actions and compact choices.

When to Use

Use DropdownMenu when a visible button opens a short set of commands or menu choices. Use ContextMenu when actions belong to a right-clicked target, Select when one value is chosen for a form, and Menubar for persistent application commands. Do not use a menu as ordinary site navigation when native links in a NavList are sufficient.

Features

  • Supports controlled state, modal behavior, looping, and dismissal options.
  • Opens from pointer or keyboard with the expected initial highlight.
  • Includes actions, checkbox/radio choices, groups, separators, and submenus.
  • Provides typeahead, focus restoration, collision-aware positioning, and RTL submenus.
  • Registers portalled content with parent modal focus scopes.

Import

import { DropdownMenu } from "@flowstack-ui/atom";

Anatomy

<DropdownMenu.Root>
  <DropdownMenu.Trigger />
  <DropdownMenu.Content>
    <DropdownMenu.Group>
      <DropdownMenu.Item />
      <DropdownMenu.CheckboxItem />
      <DropdownMenu.RadioGroup>
        <DropdownMenu.RadioItem />
      </DropdownMenu.RadioGroup>
    </DropdownMenu.Group>
    <DropdownMenu.Separator />
    <DropdownMenu.Sub>
      <DropdownMenu.SubTrigger />
      <DropdownMenu.SubContent>
        <DropdownMenu.Item />
      </DropdownMenu.SubContent>
    </DropdownMenu.Sub>
  </DropdownMenu.Content>
</DropdownMenu.Root>

API Reference

Root

Owns shared menu state and renders no wrapper.

PropTypeDefault
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void-
modalbooleantrue
closeOnSelectbooleantrue
loopbooleantrue
closeOnEscapebooleantrue

Trigger

Renders a native button by default. Pointer clicks toggle without an initial highlight; Enter, Space, and ArrowDown start at the first item, while ArrowUp starts at the last.

PropTypeDefault
disabledbooleanfalse
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"button" for custom elements
aria-haspopup"menu"
aria-expandedOpen state
aria-controlsGenerated Content ID
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"dropdown-menu-trigger"
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Content

Renders the portalled vertical menu, positions it against Trigger, manages highlight/typeahead, and restores focus on close.

PropTypeDefault
side"top" | "right" | "bottom" | "left""bottom"
align"start" | "center" | "end""start"
sideOffsetnumber4
loopbooleanRoot loop
ariaLabelstring-
onKeyDownCaptureKeyboardEventHandler-
ARIA attributeValues
role"menu"
aria-orientation"vertical"
aria-labelValue from ariaLabel
aria-labelledbyTrigger ID when ariaLabel is absent
Data attributeValues
[data-slot]"menu-content"
[data-state]"open" | "closed"
[data-side]Resolved side
[data-align]Resolved alignment
[data-positioned]Present after positioning

Group

Renders a semantic group for related entries.

ARIA attributeValues
role"group"
Data attributeValues
[data-slot]"menu-group"

Item

Represents one command. textValue supplies typeahead text when children are not a plain string.

PropTypeDefault
valuestringrequired
textValuestringText child or value
onSelect() => void-
disabledbooleanfalse
closeOnSelectbooleanRoot setting
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"menuitem"
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"menu-item"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-value]Item value

CheckboxItem

Represents an independent menu choice and stays open by default.

PropTypeDefault
valuestringrequired
textValuestringText child or value
checkedbooleanfalse
onCheckedChange(checked: boolean) => void-
disabledbooleanfalse
closeOnSelectbooleanfalse
ARIA attributeValues
role"menuitemcheckbox"
aria-checkedChecked state
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"menu-checkbox-item"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-checked]Present when checked
[data-value]Item value

RadioGroup

Provides one controlled value to nested RadioItems.

PropTypeDefault
valuestring-
onValueChange(value: string) => void-
ARIA attributeValues
role"group"
Data attributeValues
[data-slot]"menu-radio-group"

RadioItem

Represents one mutually exclusive choice and stays open by default.

PropTypeDefault
valuestringrequired
textValuestringText child or value
disabledbooleanfalse
closeOnSelectbooleanfalse
ARIA attributeValues
role"menuitemradio"
aria-checkedWhether its value matches RadioGroup
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"menu-radio-item"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-checked]Present when selected
[data-value]Item value

Separator

Creates a semantic horizontal boundary between entry groups.

ARIA attributeValues
role"separator"
aria-orientation"horizontal"
Data attributeValues
[data-slot]"menu-separator"

Sub

Owns controlled or uncontrolled state for one nested menu and renders no DOM.

PropTypeDefault
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void-

SubTrigger

Renders a menu item that opens SubContent by hover delay, click, or the direction-aware submenu key.

PropTypeDefault
valuestringrequired
textValuestringText child or value
disabledbooleanfalse
ARIA attributeValues
role"menuitem"
aria-haspopup"menu"
aria-expandedSub open state
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"menu-sub-trigger"
[data-state]"open" | "closed"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-value]Trigger value

SubContent

Renders a separately portalled nested menu and mirrors placement and open/close keys in RTL.

PropTypeDefault
sideOffsetnumber4
loopbooleantrue
ariaLabelstring-
ARIA attributeValues
role"menu"
aria-orientation"vertical"
aria-labelValue from ariaLabel
aria-labelledbySubTrigger ID when unlabeled
Data attributeValues
[data-slot]"menu-sub-content"
[data-state]"open" | "closed"
[data-side]Resolved side
[data-positioned]Present after positioning

The entry point also exports shared Menu context hooks for advanced custom parts. Prefer the namespaced parts for the complete behavior above.

Examples

Project Actions

import { DropdownMenu } from "@flowstack-ui/atom";

export function ProjectActions() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger>Actions</DropdownMenu.Trigger>
      <DropdownMenu.Content ariaLabel="Project actions">
        <DropdownMenu.Item value="duplicate" onSelect={() => console.log("Duplicate")}>
          Duplicate
        </DropdownMenu.Item>
        <DropdownMenu.Item value="archive" onSelect={() => console.log("Archive")}>
          Archive
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu.Root>
  );
}

Persistent View Options

import { useState } from "react";
import { DropdownMenu } from "@flowstack-ui/atom";

export function ViewOptions() {
  const [grid, setGrid] = useState(true);
  return (
    <DropdownMenu.Root closeOnSelect={false}>
      <DropdownMenu.Trigger>View</DropdownMenu.Trigger>
      <DropdownMenu.Content ariaLabel="View options">
        <DropdownMenu.CheckboxItem value="grid" checked={grid} onCheckedChange={setGrid}>
          Show grid
        </DropdownMenu.CheckboxItem>
      </DropdownMenu.Content>
    </DropdownMenu.Root>
  );
}

Accessibility

DropdownMenu follows the WAI-ARIA Menu pattern. Trigger provides the menu popup relationship, Content owns focus, and disabled entries are skipped. Use visible Trigger text that describes the menu.

KeyDescription
Enter / Space / ArrowDownOpens and highlights the first enabled entry.
ArrowUpOpens and highlights the last enabled entry.
ArrowDown / ArrowUpMoves through entries while open.
Home / EndMoves to the first or last entry.
Printable characterMoves by typeahead label.
ArrowRightOpens a submenu in LTR; closes it in RTL.
ArrowLeftCloses a submenu in LTR; opens it in RTL.
Enter / SpaceActivates the highlighted entry.
EscapeCloses the topmost submenu or menu.
TabCloses and restores focus to Trigger.

Changelog

See CHANGELOG.md.