Primitives
Dropdown Menu
A short list of actions or options that opens from a trigger.
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
toast,
} from 'ferry-ui'
import { Archive, ChevronDown, Copy, Pencil, Share2 } from 'lucide-react'
export default function DropdownMenuHero() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button iconRight={<ChevronDown />}>Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Project</DropdownMenuLabel>
<DropdownMenuItem onSelect={() => toast('Edit the project')}>
<Pencil /> Edit
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => toast.success('Project duplicated')}>
<Copy /> Duplicate
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => toast('Share the project')}>
<Share2 /> Share
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => toast.success('Project archived')}>
<Archive /> Archive
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}destructive item opens a Confirm Dialog. It does not delete on a click.Import the parts. Put them together in this order.
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from 'ferry-ui'
<DropdownMenu>
<DropdownMenuTrigger />
<DropdownMenuContent>
<DropdownMenuLabel />
<DropdownMenuGroup>
<DropdownMenuItem>
<DropdownMenuShortcut />
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem />
<DropdownMenuRadioGroup>
<DropdownMenuRadioItem />
</DropdownMenuRadioGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger />
<DropdownMenuPortal>
<DropdownMenuSubContent />
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>| Part | Role |
|---|---|
DropdownMenu | Holds the open state. |
DropdownMenuTrigger | Opens the menu. |
DropdownMenuContent | The panel. |
DropdownMenuItem | One action. |
DropdownMenuCheckboxItem | An option that is on or off. |
DropdownMenuRadioGroup | Holds the radio items and their value. |
DropdownMenuRadioItem | One option of a radio group. |
DropdownMenuLabel | A heading above a group of items. |
DropdownMenuSeparator | A line between two groups. |
DropdownMenuShortcut | A key hint at the right of an item. It binds no key. |
DropdownMenuGroup | Groups items for screen readers. It has no style. |
DropdownMenuSub | Holds a submenu. |
DropdownMenuSubTrigger | The item that opens the submenu. |
DropdownMenuSubContent | The panel of the submenu. |
DropdownMenuPortal | Renders the panel of a submenu in document.body. |
With asChild, DropdownMenuTrigger gives its behavior to its child element. Use it with a Button. With asChild, DropdownMenuItem renders its child element, for example a link.
Run the action of an item in onSelect. An item can hold an icon and a DropdownMenuShortcut. Set disabled on an action that is not available.
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
toast,
useModKey,
} from 'ferry-ui'
import { Copy, Download, MoreHorizontal, Send } from 'lucide-react'
export default function DropdownMenuItems() {
const mod = useModKey()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" icon={<MoreHorizontal />} aria-label="Actions for INV-2041" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel>Invoice INV-2041</DropdownMenuLabel>
<DropdownMenuGroup>
<DropdownMenuItem onSelect={() => toast.success('Invoice duplicated')}>
<Copy /> Duplicate
<DropdownMenuShortcut>{mod} D</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => toast('The download starts')}>
<Download /> Download PDF
</DropdownMenuItem>
{/* The customer paid this invoice: the action is not available. */}
<DropdownMenuItem disabled>
<Send /> Send a reminder
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => toast('Open the customer')}>View customer</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}DropdownMenuCheckboxItem takes checked and onCheckedChange. To keep the menu open after a click, call event.preventDefault() in onSelect.
The inset prop aligns a label or a plain item with the checkbox items.
import * as React from 'react'
import {
Button,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ferry-ui'
import { Settings2 } from 'lucide-react'
const DEFAULT_COLUMNS = { email: true, role: true, status: false }
export default function DropdownMenuCheckboxItems() {
const [columns, setColumns] = React.useState(DEFAULT_COLUMNS)
function toggle(key: keyof typeof DEFAULT_COLUMNS) {
return (checked: boolean) => setColumns((current) => ({ ...current, [key]: checked }))
}
// `preventDefault` keeps the menu open, so the user can change more than one column.
const keepOpen = (event: Event) => event.preventDefault()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button icon={<Settings2 />}>View</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel inset>Columns</DropdownMenuLabel>
<DropdownMenuCheckboxItem checked disabled>
Name
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={columns.email} onCheckedChange={toggle('email')} onSelect={keepOpen}>
Email
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={columns.role} onCheckedChange={toggle('role')} onSelect={keepOpen}>
Role
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={columns.status} onCheckedChange={toggle('status')} onSelect={keepOpen}>
Status
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
{/* `inset` aligns the text of a plain item with the checkbox items. */}
<DropdownMenuItem inset onSelect={() => setColumns(DEFAULT_COLUMNS)}>
Reset columns
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}DropdownMenuRadioGroup holds one value. Pass value and onValueChange.
import * as React from 'react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from 'ferry-ui'
import { ArrowUpDown } from 'lucide-react'
const LABELS: Record<string, string> = {
newest: 'Newest first',
oldest: 'Oldest first',
name: 'Name (A to Z)',
}
export default function DropdownMenuRadioItems() {
const [sort, setSort] = React.useState('newest')
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button icon={<ArrowUpDown />}>Sort: {LABELS[sort]}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel inset>Sort by</DropdownMenuLabel>
<DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
<DropdownMenuRadioItem value="newest">Newest first</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="oldest">Oldest first</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="name">Name (A to Z)</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="amount" disabled>
Amount
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Put DropdownMenuSubTrigger and DropdownMenuSubContent in DropdownMenuSub. Wrap the content in DropdownMenuPortal. Use one level of submenu only.
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
toast,
} from 'ferry-ui'
import { ChevronDown, FolderInput, Pencil } from 'lucide-react'
const TEAMS = ['Marketing', 'Product', 'Finance']
export default function DropdownMenuSubmenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button iconRight={<ChevronDown />}>Document</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem onSelect={() => toast('Rename the document')}>
<Pencil /> Rename
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<FolderInput /> Move to
</DropdownMenuSubTrigger>
{/* The portal makes sure that the first panel does not cut the submenu. */}
<DropdownMenuPortal>
<DropdownMenuSubContent className="w-40">
{TEAMS.map((team) => (
<DropdownMenuItem key={team} onSelect={() => toast.success(`Document moved to ${team}`)}>
{team}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
)
}The align prop of DropdownMenuContent aligns the panel to the start, the center or the end of the trigger. The side prop sets the side where the panel opens. Set the width with a class such as w-48.
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ferry-ui'
const ALIGNMENTS = ['start', 'center', 'end'] as const
export default function DropdownMenuAlign() {
return (
<>
{ALIGNMENTS.map((align) => (
<DropdownMenu key={align}>
<DropdownMenuTrigger asChild>
<Button>{align}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={align} className="w-48">
<DropdownMenuItem>Open</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem>Archive</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
))}
</>
)
}A menu holds its open state by default. To control the state, pass open and onOpenChange.
Pass modal={false} to keep the page interactive while the menu is open.
import * as React from 'react'
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, toast } from 'ferry-ui'
import { ChevronDown, ChevronUp } from 'lucide-react'
export default function DropdownMenuControlled() {
const [open, setOpen] = React.useState(false)
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
{/* The trigger reads the state: the arrow changes while the menu is open. */}
<Button iconRight={open ? <ChevronUp /> : <ChevronDown />}>Export</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-40">
<DropdownMenuItem onSelect={() => toast.success('CSV export ready')}>CSV file</DropdownMenuItem>
<DropdownMenuItem onSelect={() => toast.success('PDF export ready')}>PDF file</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}Give variant="destructive" to an item that deletes or revokes. The item opens a Confirm Dialog with a controlled open.
import * as React from 'react'
import {
Button,
ConfirmDialog,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
toast,
} from 'ferry-ui'
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react'
export default function DropdownMenuConfirm() {
const [confirmOpen, setConfirmOpen] = React.useState(false)
return (
<>
<span className="font-mono text-[13px] text-foreground">Analytics export</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon-tiny"
icon={<MoreHorizontal />}
aria-label="Actions for the API key Analytics export"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => toast('Rename the API key')}>
<Pencil /> Rename
</DropdownMenuItem>
<DropdownMenuSeparator />
{/* The item opens the confirmation. It does not revoke the key on a click. */}
<DropdownMenuItem variant="destructive" onSelect={() => setConfirmOpen(true)}>
<Trash2 /> Revoke key
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ConfirmDialog
open={confirmOpen}
onOpenChange={setConfirmOpen}
title="Revoke the API key “Analytics export”?"
description="Requests with this key fail from now on. This cannot be undone."
confirmLabel="Revoke key"
onConfirm={() => {
toast.success('API key revoked')
}}
/>
</>
)
}aria-label that names the target, for example "Actions for INV-2041".onSelect, not in onClick. Then the keyboard selection works.Each part also accepts the props of its Radix UI primitive and the attributes of its element.
| Prop | Type | Default |
|---|---|---|
children | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
dir | "ltr" | "rtl" | - |
| See the element or the Radix UI primitive this part renders. | ||
open | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
defaultOpen | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onOpenChange | ((open: boolean) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
modal | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onEscapeKeyDown | ((event: KeyboardEvent) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
onPointerDownOutside | ((event: PointerDownOutsideEvent) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
onFocusOutside | ((event: FocusOutsideEvent) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
onInteractOutside | ((event: FocusOutsideEvent | PointerDownOutsideEvent) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
onCloseAutoFocus | ((event: Event) => void) | - |
Event handler called when auto-focusing on close. Can be prevented. | ||
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||
loop | boolean | false |
Whether keyboard navigation should loop around | ||
side | "top" | "right" | "bottom" | "left" | - |
| See the element or the Radix UI primitive this part renders. | ||
sideOffset | number | 4 |
| See the element or the Radix UI primitive this part renders. | ||
align | "center" | "start" | "end" | - |
| See the element or the Radix UI primitive this part renders. | ||
alignOffset | number | - |
| See the element or the Radix UI primitive this part renders. | ||
arrowPadding | number | - |
| See the element or the Radix UI primitive this part renders. | ||
avoidCollisions | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
collisionBoundary | Boundary | Boundary[] | - |
| See the element or the Radix UI primitive this part renders. | ||
collisionPadding | number | Partial<Record<"top" | "right" | "bottom" | "left", number>> | - |
| See the element or the Radix UI primitive this part renders. | ||
sticky | "partial" | "always" | - |
| See the element or the Radix UI primitive this part renders. | ||
hideWhenDetached | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
updatePositionStrategy | "always" | "optimized" | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
inset | boolean | - |
Adds left padding so the text lines up with checkbox / radio items and inset labels. | ||
variant | "default" | "destructive" | default |
| ||
disabled | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onSelect | ((event: Event) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
textValue | string | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
disabled | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onSelect | ((event: Event) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
checked | "indeterminate" | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
textValue | string | - |
| See the element or the Radix UI primitive this part renders. | ||
onCheckedChange | ((checked: boolean) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
value | string | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onValueChange | ((value: string) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
valueRequired | string | - |
| See the element or the Radix UI primitive this part renders. | ||
disabled | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onSelect | ((event: Event) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
textValue | string | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
inset | boolean | - |
Adds left padding so the text lines up with checkbox / radio items. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
DropdownMenuShortcut has no props of its own. It accepts the attributes of the element it renders.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
children | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
open | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
defaultOpen | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onOpenChange | ((open: boolean) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
inset | boolean | - |
Adds left padding so the text lines up with checkbox / radio items. | ||
disabled | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
textValue | string | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onEscapeKeyDown | ((event: KeyboardEvent) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
onPointerDownOutside | ((event: PointerDownOutsideEvent) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
onFocusOutside | ((event: FocusOutsideEvent) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
onInteractOutside | ((event: FocusOutsideEvent | PointerDownOutsideEvent) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||
loop | boolean | false |
Whether keyboard navigation should loop around | ||
sideOffset | number | - |
| See the element or the Radix UI primitive this part renders. | ||
align | "start" | "end" | - |
Controls the direction the subcontent appears from its anchor menu item Default: start | ||
alignOffset | number | - |
| See the element or the Radix UI primitive this part renders. | ||
arrowPadding | number | - |
| See the element or the Radix UI primitive this part renders. | ||
avoidCollisions | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
collisionBoundary | Boundary | Boundary[] | - |
| See the element or the Radix UI primitive this part renders. | ||
collisionPadding | number | Partial<Record<"top" | "right" | "bottom" | "left", number>> | - |
| See the element or the Radix UI primitive this part renders. | ||
sticky | "partial" | "always" | - |
| See the element or the Radix UI primitive this part renders. | ||
hideWhenDetached | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
updatePositionStrategy | "always" | "optimized" | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
children | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
container | Element | DocumentFragment | null | - |
Specify a container element to portal the content into. | ||
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||