Context menu
The row menu, opened with a right-click, a long press or Shift+F10. It holds the selection bar's actions for the rows it opened on, plus Open and Copy link for one row.
Playground
Right-click the row, long-press it on a touch screen, or focus it and press Shift+F10. Turn on disabled and the browser's own menu shows instead. The code under the preview lists only the props you changed.
import { ContextMenu } from '@/ui/context-menu';<ContextMenu />One row
Right-click a company. The menu opens at the pointer with Open and Copy link first, then actions for this record: Find website shows only for a company without one. The row takes the current fill while its menu is open.
import { ArchiveIcon, ArrowSquareOutIcon, GlobeIcon, LinkIcon } from '@phosphor-icons/react';
import { CompanyLogo } from '@/ui/company-logo';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from '@/ui/context-menu';
const companies = [
{ name: 'Halcyon Bank', website: 'halcyonbank.com' },
{ name: 'Orbital Freight', website: '' },
{ name: 'Northbridge Talent', website: 'northbridge.io' },
];
export default function ContextMenuDefault() {
return (
<div className="w-90 rounded-large bg-surface shadow-raised">
{companies.map((company) => (
<ContextMenu key={company.name}>
<ContextMenuTrigger
render={<div tabIndex={0} />}
className="flex h-9 items-center gap-100 border-b border-border px-150 text-body font-medium last:border-b-0"
>
<CompanyLogo name={company.name} />
<span className="grow">{company.name}</span>
<span className="font-normal text-text-subtle">{company.website}</span>
</ContextMenuTrigger>
<ContextMenuContent aria-label={`Actions for ${company.name}`}>
<ContextMenuItem>
<ArrowSquareOutIcon />
Open
</ContextMenuItem>
<ContextMenuItem>
<LinkIcon />
Copy link
</ContextMenuItem>
{!company.website && (
<ContextMenuItem>
<GlobeIcon />
Find website
</ContextMenuItem>
)}
<ContextMenuSeparator />
<ContextMenuItem>
<ArchiveIcon />
Archive
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
))}
</div>
);
}Several rows and a submenu
On a selection, the menu acts on every selected row and says how many: Archive 3 applications. Open and Copy link drop out. The submenu opens to the right with its first item level with its parent, and the parent keeps the current fill. Danger items go last, after a separator.
import { ArchiveIcon, ArrowsLeftRightIcon, TrashIcon } from '@phosphor-icons/react';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from '@/ui/context-menu';
export default function ContextMenuSelection() {
return (
<ContextMenu>
<ContextMenuTrigger
render={<div tabIndex={0} />}
className="flex h-24 w-90 items-center justify-center rounded-large bg-background-selected text-body font-medium text-brand-bold"
>
3 applications selected. Right-click here.
</ContextMenuTrigger>
<ContextMenuContent aria-label="Actions for 3 applications">
<ContextMenuSub>
<ContextMenuSubTrigger>
<ArrowsLeftRightIcon />
Move 3 applications to stage
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>CV sent</ContextMenuItem>
<ContextMenuItem>Interview</ContextMenuItem>
<ContextMenuItem>Offer</ContextMenuItem>
<ContextMenuItem>Rejected</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem>
<ArchiveIcon />
Archive 3 applications
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">
<TrashIcon />
Delete 3 applications
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}When things go wrong
Long and non-Latin names cut off on the row and wrap in the menu. When the selected rows share no stage, the submenu holds one disabled line that says why instead of an empty list. A failed action says what happened in an error toast that stays until closed. The menu closes on the first press, so an item never runs twice.
import { ArchiveIcon, ArrowsLeftRightIcon } from '@phosphor-icons/react';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from '@/ui/context-menu';
import { toast } from '@/ui/toast';
// Rows at different stages share none, so the submenu says why instead of listing stages. A failed archive says so in a toast that stays.
export default function ContextMenuUnhappy() {
return (
<ContextMenu>
<ContextMenuTrigger
render={<div tabIndex={0} />}
className="flex h-9 w-90 min-w-0 items-center rounded-large bg-background-selected px-150 text-body font-medium text-brand-bold"
>
<span className="truncate">王小明, Zoë O'Brien-Smith and Nguyễn Thị Minh Khai selected</span>
</ContextMenuTrigger>
<ContextMenuContent aria-label="Actions for 3 applications">
<ContextMenuSub>
<ContextMenuSubTrigger>
<ArrowsLeftRightIcon />
Move 3 applications to stage
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem disabled>These applications share no stage.</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem
onClick={() => toast.add({ type: 'error', title: 'Couldn’t archive 3 applications', description: 'Check your connection and try again.' })}
>
<ArchiveIcon />
Archive 3 applications
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}Motion
The menu uses the Popup motion. Press Replay to close and open it again.
import { ArrowSquareOutIcon, LinkIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/ui/context-menu';
// Replay closes the menu, then opens it again at the row, so the 120ms exit and the 160ms entry run back to back.
export default function ContextMenuMotion() {
const [open, setOpen] = useState(false);
const replay = () => {
setOpen(false);
setTimeout(() => setOpen(true), 400);
};
return (
<div className="flex items-center gap-150">
<ContextMenu open={open} onOpenChange={setOpen}>
<ContextMenuTrigger render={<div tabIndex={0} />} className="flex h-9 w-60 items-center rounded-large bg-surface px-150 text-body font-medium shadow-raised">
Halcyon Bank
</ContextMenuTrigger>
<ContextMenuContent aria-label="Actions for Halcyon Bank">
<ContextMenuItem>
<ArrowSquareOutIcon />
Open
</ContextMenuItem>
<ContextMenuItem>
<LinkIcon />
Copy link
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<Button variant="ghost" onClick={replay}>
Replay
</Button>
</div>
);
}Button group
Two or three related buttons set 8px apart, with the one primary last. It holds dialog and form footers, page actions and the toolbar's actions.
Dropdown menu
A list of actions or options that opens from a button: every More menu, row menu and View option. It acts on something already on screen.