Context menu
A menu opened on an object: right-click, long-press, or Shift+F10 on a focused target. While it is open, an ink keyline marks the object it acts on.
Loading the live example…
"use client"
import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/keyline/ui/context-menu"
const CLIENTS = [
{ name: "Scranton Business Park", contact: "Front desk", rep: "Dwight" },
{ name: "Lackawanna County", contact: "Procurement office", rep: "Jim" },
{ name: "Blue Cross of Pennsylvania", contact: "Accounts payable", rep: "Stanley" },
]
export function ClientList() {
const [log, setLog] = useState("Right-click a client, or focus one and press Shift+F10.")
const [archived, setArchived] = useState<string[]>([])
const shown = CLIENTS.filter((c) => !archived.includes(c.name))
return (
<div className="grid gap-4">
<ul className="m-0 grid list-none gap-2 p-0" aria-label="Clients">
{shown.map((c) => (
<li key={c.name}>
<ContextMenu>
<ContextMenuTrigger
tabIndex={0}
aria-label={`${c.name}. Actions available from the context menu.`}
className="grid gap-0.5 border border-kl-line bg-kl-paper px-4 py-3 text-[14px] outline-offset-2 focus-visible:outline-2 focus-visible:outline-current"
>
<strong>{c.name}</strong>
<span className="text-kl-ink-2">
{c.contact} · {c.rep}’s account
</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => setLog(`Calling ${c.name}, ${c.contact}.`)}>Call</ContextMenuItem>
<ContextMenuItem onClick={() => setLog(`New order started for ${c.name}.`)}>New order</ContextMenuItem>
<ContextMenuItem onClick={() => setLog(`${c.rep} will get a reminder to follow up with ${c.name} on Friday.`)}>
Remind {c.rep} to follow up
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
variant="destructive"
onClick={() => {
setArchived([...archived, c.name])
setLog(`${c.name} archived.`)
}}
>
Archive client
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</li>
))}
</ul>
{shown.length === 0 ? (
<div className="grid justify-items-start gap-3 border border-dashed border-kl-line-soft px-4 py-4 text-[14px]">
<p className="m-0">No clients left on the list. Michael would call this a crisis.</p>
<Button size="compact" onClick={() => setArchived([])}>
Restore all three
</Button>
</div>
) : null}
<p role="status" className="m-0 min-h-5 text-[14px] text-kl-ink-2">
{log}
</p>
</div>
)
}Installation
npx shadcn@latest add @keyline-ui/context-menuRun it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.
Usage
A context menu is a shortcut. Put every action somewhere visible too, such as a row’s Menu button, so people who never right-click can still reach it.
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/keyline/ui/context-menu"<ContextMenu>
<ContextMenuTrigger tabIndex={0} aria-label="Lackawanna County">
Lackawanna County
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={call}>Call</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Props
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | Default <div> | On ContextMenuTrigger: the object itself, such as a row or a card. |
tabIndex | number | Default — | On ContextMenuTrigger: 0 makes the object focusable, so Shift+F10 and the Menu key reach it. |
container | Element | Default body | On ContextMenuContent: where the menu is portalled. |
The rows are Menu’s, re-exported as ContextMenuItem, ContextMenuSeparator, ContextMenuSubmenu and the rest. Built on Base UI Context Menu.