Dropdown Menu
A menu of actions triggered by a button. Shares the `popup` and `menu` style families with context-menu, select and combobox — the only things this component declares for itself are `Root`, `Trigger` and its positioning defaults.
npx shadcn@latest add @fragiola/dropdown-menuShow code
"use client";
import { ChevronDownIcon } from "lucide-react";
import type { ReactNode } from "react";
import { Clickable } from "#/atoms/clickable";
import { DropdownMenu } from "#/ui/dropdown-menu";
// The floor is palette-surface. The trigger button and the menu content read
// roles from the floor — the portal inherits the palette from the owning
// subtree. The destructive item carries palette-danger explicitly — the one
// chromatic exception for destructive menu items.
//
// The trigger uses Clickable.Button (the button surface) instead of a
// hand-rolled <button> — the same component the rest of the library uses.
export default function DropdownMenuDemo() {
return (
<div className="palette-surface flex flex-col gap-6 rounded-lg border border-palette-line bg-palette-base p-6">
<Row label="basic + grouped label">
<DropdownMenu.Root>
<DropdownMenu.Trigger
render={<Clickable.Button variant="outline" />}
>
Open menu
<ChevronDownIcon className="size-4" />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item>
<span>Profile</span>
</DropdownMenu.Item>
<DropdownMenu.Item>
<span>Billing</span>
</DropdownMenu.Item>
<DropdownMenu.Item>
<span>Settings</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
{/* Label MUST be inside a Group — GroupLabel requires
MenuGroupContext from Base UI. */}
<DropdownMenu.Group>
<DropdownMenu.Label>Account</DropdownMenu.Label>
<DropdownMenu.Item disabled>
<span>Logout</span>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Row>
<Row label="destructive item">
<DropdownMenu.Root>
<DropdownMenu.Trigger
render={<Clickable.Button variant="outline" />}
>
Destructive
<ChevronDownIcon className="size-4" />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item>
<span>Edit</span>
</DropdownMenu.Item>
<DropdownMenu.Item>
<span>Duplicate</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
{/* palette-danger applied from outside — no tone prop */}
<DropdownMenu.Item className="palette-danger">
<span>Delete</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Row>
<Row label="submenus, checkbox, radio, shortcut">
<DropdownMenu.Root>
<DropdownMenu.Trigger
render={<Clickable.Button variant="outline" />}
>
Advanced
<ChevronDownIcon className="size-4" />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
<span>Open in…</span>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item>
<span>New tab</span>
</DropdownMenu.Item>
<DropdownMenu.Item>
<span>New window</span>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem checked>
<span>Show toolbar</span>
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem>
<span>Show status bar</span>
</DropdownMenu.CheckboxItem>
<DropdownMenu.Separator />
<DropdownMenu.RadioGroup defaultValue="light">
<DropdownMenu.Label>Theme</DropdownMenu.Label>
<DropdownMenu.RadioItem value="light">
<span>Light</span>
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="dark">
<span>Dark</span>
</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator />
<DropdownMenu.Item>
<span>Save</span>
<DropdownMenu.Shortcut>⌘S</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Row>
</div>
);
}
function Row({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="flex flex-col gap-2">
<span className="text-xs font-mono text-palette-accent/85">
{label}
</span>
<div className="flex flex-wrap items-center gap-4">{children}</div>
</div>
);
}
Style families
dropdown-menu and context-menu render from the same menu source. The parts factory (createMenuParts) takes a Base UI namespace and returns the styled wrappers — twelve of them, written once. Each component file declares only Root, Trigger, Content and SubContent with its own positioning defaults.
The namespace is injected, not imported. ContextMenu.Item === Menu.Item at runtime today, but if Base UI ever diverges, injection means each component inherits its own divergence instead of being silently pinned to the wrong primitive.
Destructive tone
There is no tone, variant or destructive prop. Tone arrives as a palette class applied from outside: className="palette-danger". The highlighted:bg-palette-soft and text-palette-accent rules resolve against whichever palette is active — inheritance does the work.
Parts
| Part | Source |
|---|---|
Root | Base UI Menu.Root |
Trigger | Base UI Menu.Trigger |
Content | popup.content() + positioning |
Item | menu.item() |
CheckboxItem | menu.selectableItem() |
RadioGroup / RadioItem | menu.selectableItem() |
Label | menu.label() |
Separator | menu.separator() |
Shortcut | menu.shortcut() |
Sub / SubTrigger / SubContent | menu.subTrigger() + popup.content() |
Combobox
The second recombination test. This component is `field` + `popup` + `menu`, and it declares no new style. Every class comes from a family. If this file contained a class that did not come from a family, the recombination claim would be wrong. It does not.
Context Menu
A menu of actions triggered by a right-click. Shares the `popup` and `menu` style families with dropdown-menu — installing it after `dropdown-menu` writes exactly one file.