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

bash
npx @smicolon/fasla-ui@latest add dropdown-menu

Run 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 menu role and each row menuitem, menuitemcheckbox or menuitemradio, 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 a DropdownMenuGroup or DropdownMenuRadioGroup so 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

PropTypeDefaultDescription
openboolean—Opens or closes the menu, when controlled.
defaultOpenbooleanfalseOpens the menu on first render.
onOpenChange(open, eventDetails) => void—Called when the menu opens or closes.
modalbooleantrueWhile the menu is open, blocks presses outside it and the page's scroll. Pass false to keep the page live.
disabledbooleanfalseStops the menu from opening.
childrenReactNode—The content of this part.

DropdownMenuTrigger

PropTypeDefaultDescription
renderReactElement—Replaces the default button with your element, such as a Button.
disabledbooleanfalseStops this trigger from opening the menu.
openOnHoverbooleanfalseAlso opens the menu when the pointer rests on the trigger.
delaynumber100How long to wait before opening on hover, in milliseconds.
closeDelaynumber0How long to wait before closing a menu opened on hover, in milliseconds.
childrenReactNode—The content of this part.
classNamestring—Extra classes for this part.

DropdownMenuContent

PropTypeDefaultDescription
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.
sideOffsetnumber4Distance from the trigger to the menu, in px.
alignOffsetnumber0Moves 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.
containerHTMLElement—The element the menu and its submenus render into. Defaults to body.
childrenReactNode—The content of this part.
classNamestring—Extra classes for the menu, such as a width.

DropdownMenuLabel

PropTypeDefaultDescription
insetbooleanfalseLines the text up with rows that have an icon or an indicator.
childrenReactNode—The content of this part.
classNamestring—Extra classes for this part.

DropdownMenuItem

PropTypeDefaultDescription
variant"default" | "destructive""default"destructive marks an action that deletes something or cannot be undone.
iconReactNode—A 16px icon before the text.
insetbooleanfalseLines the text up with rows that have an icon or an indicator.
disabledbooleanfalseDims the row and stops it being pressed.
onClick(event) => void—Called when the row is pressed or chosen with the keyboard.
closeOnClickbooleantrueCloses the menu when the row is pressed.
labelstring—The text that typing a letter matches. Defaults to the row's text.
childrenReactNode—The content of this part.
classNamestring—Extra classes for this part.

DropdownMenuCheckboxItem

PropTypeDefaultDescription
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.
checkedboolean—Whether the row is on, when controlled.
defaultCheckedbooleanfalseWhether the row is on at first.
onCheckedChange(checked, eventDetails) => void—Called with the new state when the row is toggled.
disabledbooleanfalseDims the row and stops it being pressed.
closeOnClickbooleanfalseCloses the menu when the row is pressed. Off by default, so several can be changed in one go.
labelstring—The text that typing a letter matches. Defaults to the row's text.
childrenReactNode—The content of this part.
classNamestring—Extra classes for this part.

DropdownMenuRadioGroup

PropTypeDefaultDescription
valueunknown—The chosen value, when controlled.
defaultValueunknown—The value chosen on first render.
onValueChange(value, eventDetails) => void—Called with the new value when a row is chosen.
disabledbooleanfalseStops every row in the group from being chosen.
childrenReactNode—The content of this part.
classNamestring—Extra classes for this part.

DropdownMenuRadioItem

PropTypeDefaultDescription
valueunknown—The value this row sets on its group.
disabledbooleanfalseDims the row and stops it being pressed.
closeOnClickbooleanfalseCloses the menu when the row is pressed. Off by default, so several can be changed in one go.
labelstring—The text that typing a letter matches. Defaults to the row's text.
childrenReactNode—The content of this part.
classNamestring—Extra classes for this part.

DropdownMenuSub

PropTypeDefaultDescription
openboolean—Opens or closes the submenu, when controlled.
defaultOpenbooleanfalseOpens the submenu on first render.
onOpenChange(open, eventDetails) => void—Called when the submenu opens or closes.
disabledbooleanfalseStops the submenu from opening.
childrenReactNode—The content of this part.

DropdownMenuSubTrigger

PropTypeDefaultDescription
iconReactNode—A 16px icon before the text.
insetbooleanfalseLines the text up with rows that have an icon or an indicator.
disabledbooleanfalseDims the row and stops it being pressed.
openOnHoverbooleantrueOpens the submenu when the pointer rests on its row.
delaynumber100How long to wait before opening on hover, in milliseconds.
closeDelaynumber0How long to wait before closing a menu opened on hover, in milliseconds.
labelstring—The text that typing a letter matches. Defaults to the row's text.
childrenReactNode—The content of this part.
classNamestring—Extra classes for this part.

DropdownMenuSubContent

PropTypeDefaultDescription
sideOffsetnumber0Distance from the row to the submenu, in px.
alignOffsetnumber-5Moves 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.
containerHTMLElement—The element the submenu renders into. Defaults to the menu's.
childrenReactNode—The content of this part.
classNamestring—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

tsx
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

tsx
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

tsx
"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

tsx
"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

tsx
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>
  )
}