Menu
A list of commands that docks to its trigger: its top edge lies on the trigger’s keyline and it unfolds from there. Rows can open submenus or hold checkbox and radio choices.
Loading the live example…
"use client"
import { ChevronDown } from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import {
Menu,
MenuContent,
MenuItem,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuSubmenu,
MenuSubmenuContent,
MenuSubmenuTrigger,
MenuTrigger,
} from "@/components/keyline/ui/menu"
const REPS = ["Dwight Schrute", "Jim Halpert", "Phyllis Vance", "Stanley Hudson"]
export function OrderActions() {
const [rep, setRep] = useState("Dwight Schrute")
const [due, setDue] = useState("Thu 12 Nov")
const [copies, setCopies] = useState(0)
const [cancelled, setCancelled] = useState(false)
const [last, setLast] = useState("Nothing changed yet.")
return (
<div className="grid gap-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<dl className="m-0 grid grid-cols-[auto_1fr] gap-x-6 gap-y-1 text-[14px]">
<dt className="text-kl-ink-3">Order</dt>
<dd className="m-0">
<span className="font-kl-mono whitespace-nowrap">DM-4102</span> · Scranton Business Park
{cancelled ? <strong className="ms-2">Cancelled</strong> : null}
</dd>
<dt className="text-kl-ink-3">Sales rep</dt>
<dd className="m-0">{rep}</dd>
<dt className="text-kl-ink-3">Delivery</dt>
<dd className="m-0">{due}</dd>
</dl>
<Menu>
<MenuTrigger render={<Button />}>
Actions
<ChevronDown size={16} aria-hidden="true" />
</MenuTrigger>
<MenuContent align="end">
<MenuItem
disabled={cancelled}
onClick={() => {
setDue("Mon 16 Nov")
setLast("Delivery moved to Monday. The client gets an email.")
}}
>
Move delivery to Monday
</MenuItem>
<MenuItem
onClick={() => {
setCopies(copies + 1)
setLast(`Duplicated as DM-${4141 + copies}, same paper and client.`)
}}
>
Duplicate order
</MenuItem>
<MenuSubmenu>
<MenuSubmenuTrigger disabled={cancelled}>Assign to</MenuSubmenuTrigger>
<MenuSubmenuContent>
<MenuRadioGroup
value={rep}
onValueChange={(r: string) => {
setRep(r)
setLast(`${r.split(" ")[0]} has the account now.`)
}}
>
{REPS.map((r) => (
<MenuRadioItem key={r} value={r}>
{r}
</MenuRadioItem>
))}
</MenuRadioGroup>
</MenuSubmenuContent>
</MenuSubmenu>
<MenuSeparator />
<MenuItem
variant="destructive"
disabled={cancelled}
onClick={() => {
setCancelled(true)
setLast("Order cancelled. 40 cases go back on the shelf.")
}}
>
Cancel order
</MenuItem>
</MenuContent>
</Menu>
</div>
<div className="flex min-h-8 flex-wrap items-center gap-3">
<p role="status" className="m-0 text-[14px] text-kl-ink-2">
{last}
</p>
{cancelled ? (
<Button
size="compact"
variant="quiet"
onClick={() => {
setCancelled(false)
setLast("Order restored. Nothing went back on the shelf.")
}}
>
Undo
</Button>
) : null}
</div>
</div>
)
}Installation
npx shadcn@latest add @keyline-ui/menuRun it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.
Usage
MenuTrigger renders whatever you pass to render; a Keyline Button is the usual choice.
import { Button } from "@/components/keyline/ui/button"
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@/components/keyline/ui/menu"<Menu>
<MenuTrigger render={<Button />}>Actions</MenuTrigger>
<MenuContent>
<MenuItem onClick={duplicate}>Duplicate order</MenuItem>
<MenuItem variant="destructive" onClick={cancel}>Cancel order</MenuItem>
</MenuContent>
</Menu>Examples
Checkbox and radio
MenuCheckboxItem takes a tick, MenuRadioItem a filled square, so the kind reads without colour. Choosing one keeps the menu open.
Loading the live example…
"use client"
import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import {
Menu,
MenuCheckboxItem,
MenuContent,
MenuGroup,
MenuGroupLabel,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuTrigger,
} from "@/components/keyline/ui/menu"
const EVENTS = { shipped: "An order ships", complaint: "A client complains", mention: "Someone mentions me" }
type Event = keyof typeof EVENTS
export function NotificationMenu() {
const [on, setOn] = useState<Record<Event, boolean>>({ shipped: true, complaint: true, mention: false })
const [when, setWhen] = useState("now")
const chosen = (Object.keys(EVENTS) as Event[]).filter((e) => on[e])
return (
<div className="grid gap-4">
<Menu>
<MenuTrigger render={<Button className="justify-self-start" />}>Notify me</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuGroupLabel>When</MenuGroupLabel>
{(Object.keys(EVENTS) as Event[]).map((e) => (
<MenuCheckboxItem key={e} checked={on[e]} onCheckedChange={(c) => setOn({ ...on, [e]: c })}>
{EVENTS[e]}
</MenuCheckboxItem>
))}
</MenuGroup>
<MenuSeparator />
<MenuRadioGroup value={when} onValueChange={setWhen}>
<MenuGroupLabel>How often</MenuGroupLabel>
<MenuRadioItem value="now">Right away</MenuRadioItem>
<MenuRadioItem value="daily" description="One email at 5 PM">
Daily digest
</MenuRadioItem>
</MenuRadioGroup>
</MenuContent>
</Menu>
<p role="status" className="m-0 min-h-10 max-w-[52ch] text-[14px] text-kl-ink-2">
{chosen.length
? `${when === "now" ? "Right away" : "Once a day"}: ${chosen.map((e) => EVENTS[e].toLowerCase()).join(", ")}.`
: "No notifications. You’ll hear about it at the next conference room meeting."}
</p>
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "end" | Default "start" | On MenuContent: which edge of the trigger the menu lines up with. |
side | "bottom" | "top" | Default "bottom" | On MenuContent: the trigger edge it docks to. |
attachWeight | 1 | 2 | 3 | Default 2 | On MenuContent: the weight of the edge it shares. Use 1 when the trigger is a cell. |
anchor | Element | Ref | Default the trigger | On MenuContent: dock to another element, such as the frame around the trigger. |
variant | "default" | "destructive" | Default "default" | On MenuItem: destructive sets the row in red. |
description | ReactNode | Default — | On items: a second line under the label. |
shortcut | ReactNode | Default — | On items: a key shown at the row’s end. Showing it does not bind it. |
Parts: Menu, MenuTrigger, MenuContent, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuGroup, MenuGroupLabel, MenuSubmenu, MenuSubmenuTrigger, MenuSubmenuContent. Built on Base UI Menu.