Component
Dropdown Menu
A raised white menu of 36px rows with filled glyphs, lilac highlight, purple section labels and checks.
npx -y github:anwarlemu/inkbox-brand-cli#release add dropdown-menuUsage
Use for actions on one thing behind a single control (an agent's row actions, the account menu), and for small option sets with DropdownMenuCheckboxItem or DropdownMenuRadioItem. Mark a destructive action with variant="destructive" and keep it last, after a separator.
Avoid
- Navigation between pages in a dropdown
- More than one level of submenu
- Animating the highlighted row
- Icons that move on hover
Exports: DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger. This component is a client component.
Dependencies: surface, radix-ui, @phosphor-icons/react.
Example
"use client";import * as React from "react";import { BuildingsIcon, DotsThreeIcon, GearSixIcon, KeyIcon, PencilSimpleIcon, QuestionIcon, SignOutIcon, TrashIcon, UsersThreeIcon } from "@phosphor-icons/react/ssr";import { Button } from "@/components/ui/button";import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu";function AccountItems() { return ( <> <DropdownMenuLabel>Robert Ligthart</DropdownMenuLabel> <DropdownMenuGroup> <DropdownMenuItem> <BuildingsIcon weight="fill" /> My Organisation <DropdownMenuShortcut>⇧⌘O</DropdownMenuShortcut> </DropdownMenuItem> <DropdownMenuItem> <KeyIcon weight="fill" /> API Keys <DropdownMenuShortcut>⌘K</DropdownMenuShortcut> </DropdownMenuItem> <DropdownMenuSub> <DropdownMenuSubTrigger> <UsersThreeIcon weight="fill" /> Invite people </DropdownMenuSubTrigger> <DropdownMenuSubContent> <DropdownMenuItem>By email</DropdownMenuItem> <DropdownMenuItem>Copy invite link</DropdownMenuItem> </DropdownMenuSubContent> </DropdownMenuSub> <DropdownMenuItem> <GearSixIcon weight="fill" /> Settings </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuItem> <QuestionIcon weight="fill" /> Help </DropdownMenuItem> <DropdownMenuItem disabled>Billing (admins only)</DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem variant="destructive"> <SignOutIcon weight="fill" /> Log out </DropdownMenuItem> </> );}export function Preview() { return ( <div className="flex justify-center"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline">Account</Button> </DropdownMenuTrigger> <DropdownMenuContent align="start" className="w-60"> <AccountItems /> </DropdownMenuContent> </DropdownMenu> </div> );}export function Open() { return ( <div className="flex min-h-110 items-start justify-center"> <DropdownMenu defaultOpen> <DropdownMenuTrigger asChild> <Button variant="outline">Account</Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-60"> <AccountItems /> </DropdownMenuContent> </DropdownMenu> </div> );}export function RowActions() { return ( <div className="flex justify-center"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" aria-label="Actions for @acme"> <DotsThreeIcon weight="bold" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-48"> <DropdownMenuItem> <PencilSimpleIcon weight="fill" /> Edit agent </DropdownMenuItem> <DropdownMenuItem> <KeyIcon weight="fill" /> Rotate API key </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem variant="destructive"> <TrashIcon weight="fill" /> Delete agent </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> );}export function Options() { const [channels, setChannels] = React.useState({ email: true, phone: true, imessage: false }); const [mode, setMode] = React.useState("edge"); return ( <div className="flex justify-center"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline">Filter</Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-56"> <DropdownMenuLabel>Channels</DropdownMenuLabel> <DropdownMenuCheckboxItem checked={channels.email} onCheckedChange={(v) => setChannels((c) => ({ ...c, email: v === true }))}> Email </DropdownMenuCheckboxItem> <DropdownMenuCheckboxItem checked={channels.phone} onCheckedChange={(v) => setChannels((c) => ({ ...c, phone: v === true }))}> Phone </DropdownMenuCheckboxItem> <DropdownMenuCheckboxItem checked={channels.imessage} onCheckedChange={(v) => setChannels((c) => ({ ...c, imessage: v === true }))}> iMessage </DropdownMenuCheckboxItem> <DropdownMenuSeparator /> <DropdownMenuLabel>Tunnel TLS mode</DropdownMenuLabel> <DropdownMenuRadioGroup value={mode} onValueChange={setMode}> <DropdownMenuRadioItem value="edge">Edge (default)</DropdownMenuRadioItem> <DropdownMenuRadioItem value="passthrough">Passthrough</DropdownMenuRadioItem> </DropdownMenuRadioGroup> </DropdownMenuContent> </DropdownMenu> </div> );}Source
"use client";import * as React from "react";import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react/ssr";import { usePortalContainer } from "@/lib/surface";import { cn } from "@/lib/utils";// A menu, extrapolated from the console (Figma 427:2715): the panel is a raised white panel (8px corners, the floating// shadow) with 4px of padding; items are 36px rows with 8px corners like the rail's nav rows, a 20px glyph in the// action-glyph purple 10px from a 14px ink label, and the lilac under the highlighted row. Section labels are 13px in// the label purple ("Channels"), separators the rail's hairline, shortcuts 12px muted, destructive items red. The// panel grows out of its trigger from 96 percent with a fade; highlighting a row is instant (menus are used by keyboard// and many times a day).const CONTENT = "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-48 overflow-x-hidden overflow-y-auto rounded-control bg-surface p-1 text-fg shadow-floating outline-none data-[state=closed]:animate-pop-out data-[state=open]:animate-pop-in";const ITEM = "relative flex h-9 cursor-default items-center gap-2.5 rounded-control px-2.5 font-sans text-body leading-tight text-fg outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-subtle data-[inset]:pl-10.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5.5 [&_svg:not([class*='text-'])]:text-icon-secondary";function DropdownMenu(props: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;}function DropdownMenuPortal(props: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { const container = usePortalContainer(); return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" container={container} {...props} />;}function DropdownMenuTrigger(props: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;}function DropdownMenuContent({ className, sideOffset = 6, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { return ( <DropdownMenuPortal> <DropdownMenuPrimitive.Content data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn(CONTENT, "origin-(--radix-dropdown-menu-content-transform-origin)", className)} {...props} /> </DropdownMenuPortal> );}function DropdownMenuGroup(props: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;}function DropdownMenuItem({ className, inset, variant = "default", ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { inset?: boolean; variant?: "default" | "destructive" }) { return ( <DropdownMenuPrimitive.Item data-slot="dropdown-menu-item" data-inset={inset || undefined} data-variant={variant} className={cn(ITEM, "data-[variant=destructive]:text-danger data-[variant=destructive]:[&_svg]:text-danger", className)} {...props} /> );}function DropdownMenuCheckboxItem({ className, children, checked, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { return ( <DropdownMenuPrimitive.CheckboxItem data-slot="dropdown-menu-checkbox-item" checked={checked} className={cn(ITEM, "pl-10.5", className)} {...props}> <span className="pointer-events-none absolute left-2.5 flex size-5.5 items-center justify-center"> <DropdownMenuPrimitive.ItemIndicator> <CheckIcon weight="bold" className="size-4.5 text-fg-brand" /> </DropdownMenuPrimitive.ItemIndicator> </span> {children} </DropdownMenuPrimitive.CheckboxItem> );}function DropdownMenuRadioGroup(props: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;}// The selected choice wears the console's radio mark: a pink ring around white.function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { return ( <DropdownMenuPrimitive.RadioItem data-slot="dropdown-menu-radio-item" className={cn(ITEM, "pl-10.5", className)} {...props}> <span className="pointer-events-none absolute left-2.5 flex size-5.5 items-center justify-center"> <DropdownMenuPrimitive.ItemIndicator> <span className="block size-3.5 rounded-pill border-[3.5px] border-pink bg-surface" /> </DropdownMenuPrimitive.ItemIndicator> </span> {children} </DropdownMenuPrimitive.RadioItem> );}function DropdownMenuLabel({ className, inset, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }) { return <DropdownMenuPrimitive.Label data-slot="dropdown-menu-label" data-inset={inset || undefined} className={cn("px-2.5 pt-2 pb-1 font-sans text-body-sm leading-tight text-fg-label data-[inset]:pl-10.5", className)} {...props} />;}function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { return <DropdownMenuPrimitive.Separator data-slot="dropdown-menu-separator" className={cn("-mx-1 my-1 h-px bg-line-subtle", className)} {...props} />;}function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { return <span data-slot="dropdown-menu-shortcut" className={cn("ml-auto pl-4 font-sans text-caption text-fg-muted", className)} {...props} />;}function DropdownMenuSub(props: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;}function DropdownMenuSubTrigger({ className, inset, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }) { return ( <DropdownMenuPrimitive.SubTrigger data-slot="dropdown-menu-sub-trigger" data-inset={inset || undefined} className={cn(ITEM, "data-[state=open]:bg-subtle", className)} {...props}> {children} <CaretRightIcon className="ml-auto size-4.5 text-fg-muted" /> </DropdownMenuPrimitive.SubTrigger> );}function DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { return ( <DropdownMenuPortal> <DropdownMenuPrimitive.SubContent data-slot="dropdown-menu-sub-content" className={cn(CONTENT, "max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin)", className)} {...props} /> </DropdownMenuPortal> );}export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,};Reference: Extrapolated from the Inkbox console (Figma 427:2715): nav rows (38px, 8px corners, filled glyphs, lilac current row), section labels (Channels), the raised panel.