Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Dropdown Menu

Displays a menu to the user — such as a set of actions or functions — triggered by a button.

Playground

—side
—align
—sideOffset
—open
—modal
Usage
DropdownMenu wraps a DropdownMenuTrigger and a portaled DropdownMenuContent. Inside the content, DropdownMenuLabel heads a section, related DropdownMenuItems live in a DropdownMenuGroup with a DropdownMenuSeparator between groups, and a DropdownMenuShortcut renders its hint at the end of the row — wiring the actual key handler is up to you. The panel is as wide as its trigger (at least min-w-32), so give it an explicit width like w-56 when the trigger is small.
import { Button } from "@workspace/ui/components/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from "@workspace/ui/components/dropdown-menu"

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="outline">Open</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent className="w-56">
    <DropdownMenuLabel>My Account</DropdownMenuLabel>
    <DropdownMenuGroup>
      <DropdownMenuItem>
        Profile <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
      </DropdownMenuItem>
      <DropdownMenuItem>
        Billing <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
      </DropdownMenuItem>
      <DropdownMenuItem>
        Settings <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
      </DropdownMenuItem>
      <DropdownMenuItem disabled>API</DropdownMenuItem>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuItem>
      Log out <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
With icons
Drop a Phosphor icon before the item text — items lay out with gap-2 and size unclassed icons to size-4 automatically. Icons inherit the item color, including the accent foreground while the item is focused.
<DropdownMenuContent className="w-56">
  <DropdownMenuGroup>
    <DropdownMenuItem>
      <UserIcon /> Profile
    </DropdownMenuItem>
    <DropdownMenuItem>
      <CreditCardIcon /> Billing
    </DropdownMenuItem>
    <DropdownMenuItem>
      <GearSixIcon /> Settings
    </DropdownMenuItem>
  </DropdownMenuGroup>
  <DropdownMenuSeparator />
  <DropdownMenuItem>
    <SignOutIcon /> Log out
  </DropdownMenuItem>
</DropdownMenuContent>
Checkboxes
DropdownMenuCheckboxItem is a controlled toggle: pass checked and onCheckedChange, and the check indicator renders at the end of the row. The menu closes on select by default — call event.preventDefault() in onSelect to keep it open while toggling several options.
const [showStatusBar, setShowStatusBar] = React.useState(true)
const [showActivityBar, setShowActivityBar] = React.useState(false)
const [showPanel, setShowPanel] = React.useState(false)

<DropdownMenuContent className="w-56">
  <DropdownMenuLabel>Appearance</DropdownMenuLabel>
  <DropdownMenuSeparator />
  <DropdownMenuCheckboxItem
    checked={showStatusBar}
    onCheckedChange={setShowStatusBar}
  >
    Status Bar
  </DropdownMenuCheckboxItem>
  <DropdownMenuCheckboxItem
    checked={showActivityBar}
    onCheckedChange={setShowActivityBar}
    disabled
  >
    Activity Bar
  </DropdownMenuCheckboxItem>
  <DropdownMenuCheckboxItem
    checked={showPanel}
    onCheckedChange={setShowPanel}
  >
    Panel
  </DropdownMenuCheckboxItem>
</DropdownMenuContent>
Radio group
DropdownMenuRadioGroup holds the selected value and reports changes through onValueChange; each DropdownMenuRadioItem marks the active choice with the same end-of-row indicator.
const [position, setPosition] = React.useState("bottom")

<DropdownMenuContent className="w-56">
  <DropdownMenuLabel>Panel Position</DropdownMenuLabel>
  <DropdownMenuSeparator />
  <DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
    <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
  </DropdownMenuRadioGroup>
</DropdownMenuContent>
Submenu
Nest a DropdownMenuSub with its own DropdownMenuSubTrigger and DropdownMenuSubContent for a second level. The sub trigger appends a caret automatically (mirrored in RTL), highlights while its panel is open, and opens on hover, Enter or the arrow key pointing into the submenu.
<DropdownMenuContent className="w-56">
  <DropdownMenuItem>
    <UsersIcon /> Team
  </DropdownMenuItem>
  <DropdownMenuSub>
    <DropdownMenuSubTrigger>
      <UserPlusIcon /> Invite users
    </DropdownMenuSubTrigger>
    <DropdownMenuSubContent>
      <DropdownMenuItem>
        <EnvelopeSimpleIcon /> Email
      </DropdownMenuItem>
      <DropdownMenuItem>
        <ChatCircleIcon /> Message
      </DropdownMenuItem>
      <DropdownMenuSeparator />
      <DropdownMenuItem>
        <PlusCircleIcon /> More…
      </DropdownMenuItem>
    </DropdownMenuSubContent>
  </DropdownMenuSub>
  <DropdownMenuSeparator />
  <DropdownMenuItem>
    New Team <DropdownMenuShortcut>⌘T</DropdownMenuShortcut>
  </DropdownMenuItem>
</DropdownMenuContent>
Label and inset
When only some rows carry an icon, pass inset to the plain ones — it pads the start by the icon column (ps-8) so every label lines up. The prop exists on items, labels and sub triggers alike.
<DropdownMenuContent className="w-56">
  <DropdownMenuLabel inset>Actions</DropdownMenuLabel>
  <DropdownMenuItem>
    <UserPlusIcon /> Assign member
  </DropdownMenuItem>
  <DropdownMenuItem inset>Rename</DropdownMenuItem>
  <DropdownMenuItem inset>Duplicate</DropdownMenuItem>
</DropdownMenuContent>
Destructive
variant="destructive" paints the item — icon included — with the destructive token and swaps the focus highlight for a translucent destructive wash. Reserve it for the irreversible row and keep a separator before it.
<DropdownMenuContent className="w-56">
  <DropdownMenuItem>Edit</DropdownMenuItem>
  <DropdownMenuItem>Duplicate</DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuItem variant="destructive">
    <TrashIcon /> Delete
  </DropdownMenuItem>
</DropdownMenuContent>
Avatar trigger
Any element works as the trigger through asChild — here an icon-sized ghost button wrapping an Avatar, the classic account switcher. Align the panel to the end so it hangs off the avatar edge, and give the hidden label an sr-only span.
<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" size="icon" className="rounded-full">
      <Avatar className="size-8">
        <AvatarImage src="https://github.com/shadcn.png" alt="@westy" />
        <AvatarFallback>WD</AvatarFallback>
      </Avatar>
      <span className="sr-only">Open account menu</span>
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent className="w-56" align="end">
    <DropdownMenuLabel>
      <div className="flex flex-col gap-0.5">
        <span className="text-foreground">Westy</span>
        <span className="font-normal">hello@westy.la</span>
      </div>
    </DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuGroup>
      <DropdownMenuItem>Profile</DropdownMenuItem>
      <DropdownMenuItem>Billing</DropdownMenuItem>
      <DropdownMenuItem>Notifications</DropdownMenuItem>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuItem>Log out</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
RTL
Pass dir="rtl" on the DropdownMenu root. Items, indicators and shortcuts are laid out with logical properties (ps-8, pe-8, end-2, ms-auto), and the submenu caret flips via rtl:rotate-180 — so the whole menu mirrors without extra classes.
<DropdownMenu dir="rtl">
  <DropdownMenuTrigger asChild>
    <Button variant="outline">فتح القائمة</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent className="w-56">
    <DropdownMenuLabel>حسابي</DropdownMenuLabel>
    <DropdownMenuGroup>
      <DropdownMenuItem>
        الملف الشخصي <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
      </DropdownMenuItem>
      <DropdownMenuItem>
        الفواتير <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
      </DropdownMenuItem>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuItem>تسجيل الخروج</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
Accessibility
The Radix Menu primitive implements the WAI-ARIA menu button pattern: the trigger exposes aria-haspopup and aria-expanded, arrow keys and typeahead move focus between items, Enter or Space activates, and Escape closes and returns focus to the trigger. Checkbox and radio items announce their state via aria-checked; disabled items are skipped. DropdownMenuShortcut is a purely visual hint — it is not announced and does not register a key handler. Icon-only triggers still need an accessible name.
<DropdownMenuTrigger asChild>
  <Button variant="ghost" size="icon">
    <DotsThreeVerticalIcon />
    <span className="sr-only">Open menu</span>
  </Button>
</DropdownMenuTrigger>
API Reference
Fifteen parts wrap the Radix DropdownMenu primitive with Westy styling. DropdownMenu is the stateful root, DropdownMenuTrigger the behavior wrapper (use asChild to keep your own markup), and DropdownMenuContent handles the portal and positioning automatically — DropdownMenuPortal exists for advanced composition only. Every Radix prop passes through; see the Radix documentation for the full primitive API.
ComponentPropDefault
DropdownMenuopen?: boolean, onOpenChange?: (open: boolean) => void—
DropdownMenumodal?: booleantrue
DropdownMenudir?: "ltr" | "rtl""ltr"
DropdownMenuTriggerasChild?: booleanfalse
DropdownMenuContentside?: "top" | "right" | "bottom" | "left""bottom"
DropdownMenuContentalign?: "start" | "center" | "end""start"
DropdownMenuContentsideOffset?: number4
DropdownMenuItemvariant?: "default" | "destructive""default"
DropdownMenuIteminset?: boolean, disabled?: boolean, onSelect?: (event: Event) => void—
DropdownMenuCheckboxItemchecked?: boolean, onCheckedChange?: (checked: boolean) => void—
DropdownMenuRadioGroupvalue?: string, onValueChange?: (value: string) => void—
<DropdownMenu data-slot="dropdown-menu" />
<DropdownMenuTrigger data-slot="dropdown-menu-trigger" />
<DropdownMenuContent data-slot="dropdown-menu-content" data-side data-align />
<DropdownMenuGroup data-slot="dropdown-menu-group" />
<DropdownMenuLabel data-slot="dropdown-menu-label" data-inset />
<DropdownMenuItem data-slot="dropdown-menu-item" data-variant data-inset />
<DropdownMenuCheckboxItem data-slot="dropdown-menu-checkbox-item" data-state />
<DropdownMenuRadioGroup data-slot="dropdown-menu-radio-group" />
<DropdownMenuRadioItem data-slot="dropdown-menu-radio-item" data-state />
<DropdownMenuSeparator data-slot="dropdown-menu-separator" />
<DropdownMenuShortcut data-slot="dropdown-menu-shortcut" />
<DropdownMenuSub data-slot="dropdown-menu-sub" />
<DropdownMenuSubTrigger data-slot="dropdown-menu-sub-trigger" data-open />
<DropdownMenuSubContent data-slot="dropdown-menu-sub-content" />

Tokens used

13 design tokens consumed by the Dropdown Menu component.

  • --popover

    Popover

    bg-popover
  • --popover-foreground

    Popover Foreground

    text-popover-foreground
  • --accent

    Accent

    focus:bg-accentdata-open:bg-accent
  • --accent-foreground

    Accent Foreground

    focus:text-accent-foregrounddata-open:text-accent-foreground
  • --destructive

    Destructive

    data-[variant=destructive]:text-destructivedata-[variant=destructive]:focus:bg-destructive/10dark:data-[variant=destructive]:focus:bg-destructive/20
  • --muted-foreground

    Muted Foreground

    text-muted-foreground
  • --foreground

    Foreground

    ring-foreground/10
  • --border

    Border

    bg-border
  • --radius-md

    Radius 0.8× (8px)

    rounded-md
  • --radius-sm

    Radius 0.6× (6px)

    rounded-sm
  • --text-sm

    Text Small (14px)

    text-sm
  • --text-xs

    Text Extra Small (12px)

    text-xs
  • --spacing

    Spacing unit (4px)

    p-1px-2py-1.5gap-2ps-8pe-8min-w-32size-4