Dropdown Menu
A menu of actions and settings that opens from a button. Use it for a short list of things the reader can do with the page, or with one item on it, such as editing, sharing or signing out.
Installation
npx @smicolon/fasla-ui@latest add dropdown-menuRun init once per project before adding components. See Installation.
Dropdown Menu is built on Base UI, so it needs the @base-ui/react package. It shares its menu styles with select, and its switch rows use switch. add installs both with the component, and so does the shadcn CLI.
Preview
Press the button, or move to it with Tab and press Enter or the down arrow.
Variants
Rows come in two variants, set with variant. default is for everyday actions. destructive marks an action that deletes something or cannot be undone: put it last, after a separator, and say exactly what it does in its label. icon puts a 16px icon before the text.
States
A row is highlighted while the pointer rests on it or the arrow keys reach it; point at the rows above to see it. A row with disabled is dimmed and cannot be pressed. It stays reachable with the arrow keys, so screen readers can announce that it is unavailable.
Labels, groups and icons
DropdownMenuLabel puts a heading over some rows. Wrap the label and its rows in DropdownMenuGroup and screen readers announce the group by that heading. A label can hold more than text, such as an avatar with a name and an email. DropdownMenuSeparator draws a line between groups.
Checkbox items
DropdownMenuCheckboxItem is a setting that is on or off. Pressing it toggles it and keeps the menu open. By default a check before the text shows that it is on; its space is kept when it is off, so the text never moves. With indicator="switch", a Switch at the end of the row shows the state instead.
Radio group
DropdownMenuRadioGroup holds rows of which exactly one is chosen, like a setting with a few values. A dot before the text marks the chosen row. Pressing a row chooses it and keeps the menu open.
Submenus and shortcuts
DropdownMenuSub nests a second menu. Its DropdownMenuSubTrigger row ends in a chevron that points to the side the submenu opens on: the end of the line, so the right in English and the left in Arabic. It opens on hover, on press, and with the arrow key that points into it. DropdownMenuShortcut shows a keyboard shortcut at the end of a row, in the same order in both languages. It only shows the keys; listen for them in your own code.
Accessibility
- Base UI gives the menu the
menurole and each rowmenuitem,menuitemcheckboxormenuitemradio, so screen readers announce what each row is and whether it is checked. The trigger reports whether the menu is open. - Enter, Space or the down arrow opens the menu from the trigger. The arrow keys move between rows, Home and End jump to the first and last, and typing a letter jumps to the next row that starts with it. Escape closes the menu and returns focus to the trigger.
- Give the trigger a name that says what the menu is for; an icon-only trigger needs an
aria-label. Put labels inside aDropdownMenuGrouporDropdownMenuRadioGroupso they name their rows. - In a switch row, the row itself is the checkbox. The Switch only shows its state, so it is hidden from screen readers and takes no presses or focus of its own.
- The menu takes its direction, language and font from the trigger, even though it renders at the end of
body. On an Arabic page it lines up with the trigger's right edge, and submenus open to the left. Direction is never a prop.
Props
DropdownMenu
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Opens or closes the menu, when controlled. |
defaultOpen | boolean | false | Opens the menu on first render. |
onOpenChange | (open, eventDetails) => void | — | Called when the menu opens or closes. |
modal | boolean | true | While the menu is open, blocks presses outside it and the page's scroll. Pass false to keep the page live. |
disabled | boolean | false | Stops the menu from opening. |
children | ReactNode | — | The content of this part. |
DropdownMenuTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | — | Replaces the default button with your element, such as a Button. |
disabled | boolean | false | Stops this trigger from opening the menu. |
openOnHover | boolean | false | Also opens the menu when the pointer rests on the trigger. |
delay | number | 100 | How long to wait before opening on hover, in milliseconds. |
closeDelay | number | 0 | How long to wait before closing a menu opened on hover, in milliseconds. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for this part. |
DropdownMenuContent
| Prop | Type | Default | Description |
|---|---|---|---|
side | "bottom" | "top" | "left" | "right" | "inline-start" | "inline-end" | "bottom" | Side of the trigger the menu opens on. The inline-* sides follow the reading direction. |
align | "start" | "center" | "end" | "start" | Where along that side it lines up with the trigger. start follows the reading direction. |
sideOffset | number | 4 | Distance from the trigger to the menu, in px. |
alignOffset | number | 0 | Moves the menu along its side, in px. |
collisionAvoidance | { side, align, fallbackAxisSide } | — | How the menu avoids the window edge. By default it moves to the other side of the trigger when there is no room. { side: "none" } keeps it in place. |
container | HTMLElement | — | The element the menu and its submenus render into. Defaults to body. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for the menu, such as a width. |
DropdownMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | false | Lines the text up with rows that have an icon or an indicator. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for this part. |
DropdownMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "default" | destructive marks an action that deletes something or cannot be undone. |
icon | ReactNode | — | A 16px icon before the text. |
inset | boolean | false | Lines the text up with rows that have an icon or an indicator. |
disabled | boolean | false | Dims the row and stops it being pressed. |
onClick | (event) => void | — | Called when the row is pressed or chosen with the keyboard. |
closeOnClick | boolean | true | Closes the menu when the row is pressed. |
label | string | — | The text that typing a letter matches. Defaults to the row's text. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for this part. |
DropdownMenuCheckboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
indicator | "check" | "switch" | "check" | How the row shows that it is on: a check before the text, or a Switch at the end of the row. |
checked | boolean | — | Whether the row is on, when controlled. |
defaultChecked | boolean | false | Whether the row is on at first. |
onCheckedChange | (checked, eventDetails) => void | — | Called with the new state when the row is toggled. |
disabled | boolean | false | Dims the row and stops it being pressed. |
closeOnClick | boolean | false | Closes the menu when the row is pressed. Off by default, so several can be changed in one go. |
label | string | — | The text that typing a letter matches. Defaults to the row's text. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for this part. |
DropdownMenuRadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
value | unknown | — | The chosen value, when controlled. |
defaultValue | unknown | — | The value chosen on first render. |
onValueChange | (value, eventDetails) => void | — | Called with the new value when a row is chosen. |
disabled | boolean | false | Stops every row in the group from being chosen. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for this part. |
DropdownMenuRadioItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | unknown | — | The value this row sets on its group. |
disabled | boolean | false | Dims the row and stops it being pressed. |
closeOnClick | boolean | false | Closes the menu when the row is pressed. Off by default, so several can be changed in one go. |
label | string | — | The text that typing a letter matches. Defaults to the row's text. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for this part. |
DropdownMenuSub
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Opens or closes the submenu, when controlled. |
defaultOpen | boolean | false | Opens the submenu on first render. |
onOpenChange | (open, eventDetails) => void | — | Called when the submenu opens or closes. |
disabled | boolean | false | Stops the submenu from opening. |
children | ReactNode | — | The content of this part. |
DropdownMenuSubTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | A 16px icon before the text. |
inset | boolean | false | Lines the text up with rows that have an icon or an indicator. |
disabled | boolean | false | Dims the row and stops it being pressed. |
openOnHover | boolean | true | Opens the submenu when the pointer rests on its row. |
delay | number | 100 | How long to wait before opening on hover, in milliseconds. |
closeDelay | number | 0 | How long to wait before closing a menu opened on hover, in milliseconds. |
label | string | — | The text that typing a letter matches. Defaults to the row's text. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for this part. |
DropdownMenuSubContent
| Prop | Type | Default | Description |
|---|---|---|---|
sideOffset | number | 0 | Distance from the row to the submenu, in px. |
alignOffset | number | -5 | Moves the submenu along the row, in px. The default lines its first row up with the trigger row. |
collisionAvoidance | { side, align, fallbackAxisSide } | — | How the menu avoids the window edge. By default it moves to the other side of the trigger when there is no room. { side: "none" } keeps it in place. |
container | HTMLElement | — | The element the submenu renders into. Defaults to the menu's. |
children | ReactNode | — | The content of this part. |
className | string | — | Extra classes for the menu, such as a width. |
DropdownMenuGroup and DropdownMenuShortcut take only className and children, and DropdownMenuSeparator takes only className.
Usage
Basic menu
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function AccountMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>Open menu</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>My account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem disabled>API</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Sign out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}Icons and a destructive action
import { PencilLine, Share, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function FileActionsMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>Open menu</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem icon={<PencilLine strokeWidth={1.5} />}>Edit</DropdownMenuItem>
<DropdownMenuItem icon={<Share strokeWidth={1.5} />}>Share</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" icon={<Trash2 strokeWidth={1.5} />}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}Checkbox and switch items
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function NotificationsMenu() {
const [email, setEmail] = useState(true)
const [sms, setSms] = useState(false)
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>Notifications</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
<DropdownMenuLabel>Notify me by</DropdownMenuLabel>
<DropdownMenuCheckboxItem checked={email} onCheckedChange={setEmail}>
Email
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem indicator="switch" checked={sms} onCheckedChange={setSms}>
Text message
</DropdownMenuCheckboxItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Radio group
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function PanelPositionMenu() {
const [position, setPosition] = useState("top")
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>Panel position</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
<DropdownMenuLabel>Panel position</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="end">Right</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Submenu
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ShareMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>Open menu</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem>New tab</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Share</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Email</DropdownMenuItem>
<DropdownMenuItem>Message</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
)
}