Skip to main content

Dropdown Menu

Displays a menu to the user (such as a set of actions or functions), triggered by a button. Built on top of Radix Dropdown Menu.

import { Button } from "@ngrok/mantle/button";import { DropdownMenu } from "@ngrok/mantle/dropdown-menu";import { Icon } from "@ngrok/mantle/icon";import { Tooltip } from "@ngrok/mantle/tooltip";import { DesktopIcon } from "@phosphor-icons/react/Desktop";import { GearIcon } from "@phosphor-icons/react/Gear";import { MoonIcon } from "@phosphor-icons/react/Moon";import { SignOutIcon } from "@phosphor-icons/react/SignOut";import { SunIcon } from "@phosphor-icons/react/Sun";import { useState } from "react"; function Example() {	const [selectedTheme, setSelectedTheme] = useState("system");	const [enableNotifications, setEnableNotifications] = useState(false); 	return (		<DropdownMenu.Root>			<DropdownMenu.Trigger asChild>				<Button type="button" appearance="filled" intent="neutral">					Open Menu				</Button>			</DropdownMenu.Trigger>			<DropdownMenu.Content>				<DropdownMenu.Label>corby.pickles@ngork.com</DropdownMenu.Label>				<DropdownMenu.Separator />				<DropdownMenu.RadioGroup					value={selectedTheme}					onValueChange={(value) => {						setSelectedTheme(value);					}}				>					<DropdownMenu.RadioItem value="system">						<Icon svg={<DesktopIcon />} />						System Preference					</DropdownMenu.RadioItem>					<DropdownMenu.RadioItem value="light">						<Icon svg={<SunIcon />} />						Light Mode					</DropdownMenu.RadioItem>					<DropdownMenu.RadioItem value="dark">						<Icon svg={<MoonIcon />} />						Dark Mode					</DropdownMenu.RadioItem>				</DropdownMenu.RadioGroup>				<DropdownMenu.Separator />				<Tooltip.Root>					<Tooltip.Trigger asChild>						<DropdownMenu.Item className="flex items-center gap-2" disabled>							<Icon svg={<GearIcon />} />							User Settings						</DropdownMenu.Item>					</Tooltip.Trigger>					<Tooltip.Content side="left">Only winners get user settings.</Tooltip.Content>				</Tooltip.Root>				<DropdownMenu.Separator />				<DropdownMenu.CheckboxItem					checked={enableNotifications}					onCheckedChange={(value) => {						setEnableNotifications(value);					}}				>					Enable Notifications				</DropdownMenu.CheckboxItem>				<DropdownMenu.Group>					<DropdownMenu.Item>Team</DropdownMenu.Item>					<DropdownMenu.Sub>						<DropdownMenu.SubTrigger>Invite users</DropdownMenu.SubTrigger>						<DropdownMenu.SubContent>							<DropdownMenu.Item>Email</DropdownMenu.Item>							<DropdownMenu.Item>Message</DropdownMenu.Item>							<DropdownMenu.Separator />							<DropdownMenu.Item>More...</DropdownMenu.Item>						</DropdownMenu.SubContent>					</DropdownMenu.Sub>					<DropdownMenu.Item>						New Team						<DropdownMenu.Shortcut>⌘+T</DropdownMenu.Shortcut>					</DropdownMenu.Item>				</DropdownMenu.Group>				<DropdownMenu.Separator />				<DropdownMenu.Item className="flex items-center gap-2">					<Icon svg={<SignOutIcon />} />					Log out				</DropdownMenu.Item>			</DropdownMenu.Content>		</DropdownMenu.Root>	);}

Layering and z-index

DropdownMenu.Content and DropdownMenu.SubContent render at Tailwind z-50, Mantle's float tier. When composed inside an open Dialog, AlertDialog, or Sheet, they portal into that overlay's positioner and paint above the overlay that owns them. Outside every overlay, they portal to document.body, below the overlay tier (z-60). When sibling floats share a container, the most recently mounted float paints on top.

DropdownMenu does not render a backdrop by default. Modal layers such as Dialog, Sheet, and AlertDialog do render backdrops, so stacking those layers compounds the dim and blur effect on the layers underneath.

Composition

Compose the parts of a DropdownMenu together to build your own:

DropdownMenu.Root├── DropdownMenu.Trigger└── DropdownMenu.Content    ├── DropdownMenu.Group    │   ├── DropdownMenu.Label    │   ├── DropdownMenu.Item    │   │   └── DropdownMenu.Shortcut    │   ├── DropdownMenu.CheckboxItem    │   └── DropdownMenu.RadioGroup    │       └── DropdownMenu.RadioItem    ├── DropdownMenu.Separator    └── DropdownMenu.Sub        ├── DropdownMenu.SubTrigger        └── DropdownMenu.SubContent

Polymorphism

asChild renders the part's single child instead of its default element, and forwards class names, data-* attributes, and ref onto that child. These parts take it:

PartReach for it when
DropdownMenu.TriggerThe trigger is a Button, an IconButton, or your own control.
DropdownMenu.ContentThe panel needs its own element to carry layout or a motion wrapper.
DropdownMenu.SubContentThe same, for a submenu panel.
DropdownMenu.ItemThe item navigates, so it must render an <a> or a router Link.
DropdownMenu.CheckboxItemThe same, for a choice a URL carries.
DropdownMenu.RadioItemThe same, for one choice out of a URL-driven set.
DropdownMenu.SubTriggerThe submenu opener must be your own element.
DropdownMenu.LabelThe label needs a heading element for the document outline.
DropdownMenu.GroupThe group needs its own element.
DropdownMenu.RadioGroupThe same, for a radio group.
DropdownMenu.SeparatorThe divider must be a specific element, such as an <hr>.

DropdownMenu.SubTrigger, DropdownMenu.CheckboxItem, and DropdownMenu.RadioItem each render a second element beside your children: the submenu caret, or the check indicator. Under asChild, Radix clones your single child as the item element and moves the label span and that element inside it, so the translation guarantee holds on both paths.

API Reference

The DropdownMenu components are built on top of Radix Dropdown Menu.

The root, stateful component that manages the open/closed state of the dropdown menu.

DropdownMenu.Content and DropdownMenu.SubContent render at Tailwind z-50, Mantle's float tier. When composed inside an open Dialog, AlertDialog, or Sheet, they portal into that overlay's positioner and paint above the overlay that owns them. Outside every overlay, they portal to document.body, below the overlay tier (z-60). When sibling floats share a container, the most recently mounted float paints on top.

PropTypeDefaultDescription
open?booleanThe controlled open state of the dropdown menu. Must be used in conjunction with onOpenChange.
onOpenChange?(open: boolean) => voidEvent handler called when the open state of the dropdown menu changes.
defaultOpen?booleanfalseThe open state of the dropdown menu when it is initially rendered. Use when you do not need to control its open state.
dir?"ltr" | "rtl"The reading direction of submenus when applicable.
modal?booleantrueThe modality of the dropdown menu. When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers.

The trigger button that opens the dropdown menu.

PropTypeDefaultDescription
asChild?booleanfalseUse the asChild prop to compose the trigger functionality onto your own component.

The container for the dropdown menu content. Appears in a portal with scrolling and animations.

DropdownMenu.Content and DropdownMenu.SubContent render at Tailwind z-50, Mantle's float tier. When composed inside an open Dialog, AlertDialog, or Sheet, they portal into that overlay's positioner and paint above the overlay that owns them. Outside every overlay, they portal to document.body, below the overlay tier (z-60). When sibling floats share a container, the most recently mounted float paints on top.

All props from Radix DropdownMenu.Content, plus:

PropTypeDefaultDescription
width?"trigger" | "content"trigger will ensure the dropdown content is the same width as the trigger button. content will make the dropdown content use the intrinsic content width.
side?"top" | "right" | "bottom" | "left""bottom"The preferred side of the trigger to render against when open.
sideOffset?numberThe distance in pixels from the trigger.
align?"start" | "center" | "end""center"The preferred alignment against the trigger.
alignOffset?number0An offset in pixels from the start or end alignment options.
loop?booleantrueWhen true, keyboard navigation will loop from last item to first, and vice versa.

A standard item in the dropdown menu that can be selected or activated.

PropTypeDefaultDescription
inset?booleanWhen true, adds left padding to align with items that have icons.
disabled?booleanWhen true, prevents the user from interacting with the item.
onSelect?(event: Event) => voidEvent handler called when the user selects an item (via mouse or key). Call event.preventDefault() to keep the menu open.

A menu item with a checkbox that can be controlled or uncontrolled.

children render inside a <span data-slot="dropdown-menu-checkbox-item-label">. The check indicator is a permanent element sibling, so without the span a bare text label is never a lone child: a browser translation engine reparents that text node, and the removal React runs when the label changes shape or goes away throws. The span is display: contents, so an icon and its label stay flex items of the item and keep its gap. Browser translation covers the mechanism and the rules your own components need.

An icon you pass as a child is now a grandchild of the item, so a [&>svg] class of your own no longer reaches it. Match the part's own variant instead: [&>[data-slot=dropdown-menu-checkbox-item-label]>svg]:size-4. A [&_svg] class or a class on the icon itself loses to the item's default, which carries one more attribute selector.

All props from Radix DropdownMenu.CheckboxItem, including:

PropTypeDefaultDescription
checked?boolean | "indeterminate"The controlled checked state of the item. Must be used with onCheckedChange.
onCheckedChange?(checked: boolean) => voidEvent handler called when the checked state changes.
disabled?booleanWhen true, prevents the user from interacting with the item.

Data attributes

Data AttributeValueDescription
data-slot"dropdown-menu-checkbox-item"On the item element.
data-slot"dropdown-menu-checkbox-item-label"On the <span> wrapping children.

A radio group container for exclusive selection within the dropdown menu.

PropTypeDefaultDescription
value?stringThe value of the selected item in the group.
onValueChange?(value: string) => voidEvent handler called when the value changes.

A radio item where only one item in the group can be selected at a time.

children render inside a <span data-slot="dropdown-menu-radio-item-label">. The check indicator is a permanent element sibling, so without the span a bare text label is never a lone child: a browser translation engine reparents that text node, and the removal React runs when the label changes shape or goes away throws. The span is display: contents, so an icon and its label stay flex items of the item and keep its gap. Browser translation covers the mechanism and the rules your own components need.

An icon you pass as a child is now a grandchild of the item, so a [&>svg] class of your own no longer reaches it. Match the part's own variant instead: [&>[data-slot=dropdown-menu-radio-item-label]>svg]:size-4. A [&_svg] class or a class on the icon itself loses to the item's default, which carries one more attribute selector.

All props from Radix DropdownMenu.RadioItem, including:

PropTypeDefaultDescription
valuestringThe unique value of the item.
disabled?booleanWhen true, prevents the user from interacting with the item.

Data attributes

Data AttributeValueDescription
data-slot"dropdown-menu-radio-item"On the item element.
data-slot"dropdown-menu-radio-item-label"On the <span> wrapping children.

A label for grouping and describing sections within the dropdown menu.

PropTypeDefaultDescription
inset?booleanWhen true, adds left padding to align with items that have icons.

A group container for organizing related dropdown menu items. Accepts all props from Radix DropdownMenu.Group.

A visual separator for dividing sections within the dropdown menu. All props from Separator.

A keyboard shortcut indicator for dropdown menu items. It locks translate="no", because a translated key name names the wrong key. All props from span except translate.

A submenu container for creating nested dropdown menus. Accepts all props from Radix DropdownMenu.Sub.

PropTypeDefaultDescription
open?booleanThe controlled open state of the submenu. Must be used in conjunction with onOpenChange.
onOpenChange?(open: boolean) => voidEvent handler called when the open state of the submenu changes.
defaultOpen?booleanThe open state of the submenu when it is initially rendered.

A trigger for a dropdown menu sub-menu. It opens the submenu on hover, or on ArrowRight or Enter.

children render inside a <span data-slot="dropdown-menu-sub-trigger-label">. The caret is a permanent element sibling, so without the span a bare text label is never a lone child: a browser translation engine reparents that text node, and the removal React runs when the label changes shape or goes away throws. The span is display: contents, so an icon and its label stay flex items of the item and keep its gap. Browser translation covers the mechanism and the rules your own components need.

An icon you pass as a child is now a grandchild of the item, so a [&>svg] class of your own no longer reaches it. Match the part's own variant instead: [&>[data-slot=dropdown-menu-sub-trigger-label]>svg]:size-4. A [&_svg] class or a class on the icon itself loses to the item's default, which carries one more attribute selector.

PropTypeDefaultDescription
inset?booleanWhen true, adds left padding to align with items that have icons.

Data attributes

Data AttributeValueDescription
data-slot"dropdown-menu-sub-trigger"On the item element.
data-slot"dropdown-menu-sub-trigger-label"On the <span> wrapping children.

The content container for submenu items. Appears in a portal with scrolling and animations.

DropdownMenu.Content and DropdownMenu.SubContent render at Tailwind z-50, Mantle's float tier. When composed inside an open Dialog, AlertDialog, or Sheet, they portal into that overlay's positioner and paint above the overlay that owns them. Outside every overlay, they portal to document.body, below the overlay tier (z-60). When sibling floats share a container, the most recently mounted float paints on top.

PropTypeDefaultDescription
loop?booleantrueWhen true, keyboard navigation will loop from last item to first, and vice versa.