Menu
A dropdown menu on elevation 2 whose inset header and items share the panel's curve.
Installation
$ pnpm dlx shadcn@latest add @ssntl/menuUsage
import { Menu, MenuContent, MenuHeader, MenuItem, MenuSeparator, MenuTrigger } from "@/components/ui/menu"
<Menu>
<MenuTrigger asChild><Button variant="secondary">Product actions</Button></MenuTrigger>
<MenuContent>
<MenuHeader>Ceramic cup</MenuHeader>
<MenuItem><Pencil /> Edit</MenuItem>
<MenuSeparator />
<MenuItem destructive><Trash2 /> Delete</MenuItem>
</MenuContent>
</Menu>Rules
What this component expects from you, as it ships in components/ui/menu.md next to the code. The full file adds when to use it, keyboard, the UI foundations it applies and every shared rule.
## Rules for Menu
- **Dropdown + inset header.** The panel is `radius-md` (22) with 8px of padding, so the header and items take 14 (`radius-sm`) and nest in its curve.
- **Dropdown layer.** Elevation-2, entering with `pop-in` from the trigger's side.
- Radix handles arrow keys, typeahead, Escape and focus return. Items are 44px.
- Destructive items are red text, last, after a separator; confirm in a Dialog before they run.
- Only show a shortcut that works.Code
"use client"
import * as React from "react"
import { DropdownMenu as MenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
// A dropdown menu (Radix): keyboard, typeahead, focus return and collision
// handling come from the primitive. Elevation 2: it floats above the page.
// Nested corners: the panel is radius-md (22) with 8px of padding, so the
// inset header and every item take 22 − 8 = 14 (radius-sm) and share its
// curve.
const Menu = MenuPrimitive.Root
const MenuTrigger = MenuPrimitive.Trigger
const MenuGroup = MenuPrimitive.Group
function MenuContent({
className,
sideOffset = 8,
align = "start",
...props
}: React.ComponentProps<typeof MenuPrimitive.Content>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Content
data-slot="menu-content"
sideOffset={sideOffset}
align={align}
className={cn(
"z-50 grid max-h-(--radix-dropdown-menu-content-available-height) min-w-56 origin-(--radix-dropdown-menu-content-transform-origin) gap-0.5 overflow-y-auto overscroll-contain rounded-outer-md bg-popover p-concentric-2 text-popover-foreground elevation-2",
"data-[state=closed]:pop-out data-[state=open]:pop-in",
className
)}
{...props}
/>
</MenuPrimitive.Portal>
)
}
/** A muted card at the top: who or what the menu acts on. */
function MenuHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="menu-header"
className={cn(
"mb-1 grid gap-0.5 rounded-inner bg-muted px-3 py-2.5",
className
)}
{...props}
/>
)
}
function MenuItem({
className,
destructive = false,
...props
}: React.ComponentProps<typeof MenuPrimitive.Item> & {
/** Red text for Delete or Remove; confirm before it runs. */
destructive?: boolean
}) {
return (
<MenuPrimitive.Item
data-slot="menu-item"
className={cn(
"flex min-h-11 cursor-pointer items-center gap-3 rounded-inner px-3 text-label font-normal outline-none select-none",
"data-disabled:pointer-events-none data-disabled:opacity-40 data-highlighted:bg-secondary",
"[&_svg]:size-5 [&_svg]:shrink-0 [&_svg]:opacity-80",
destructive && "text-destructive",
className
)}
{...props}
/>
)
}
function MenuSeparator({
className,
...props
}: React.ComponentProps<typeof MenuPrimitive.Separator>) {
return (
<MenuPrimitive.Separator
data-slot="menu-separator"
className={cn("mx-3 my-1 h-px bg-border", className)}
{...props}
/>
)
}
/** A keyboard shortcut at the end of an item. */
function MenuShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="menu-shortcut"
className={cn(
"ms-auto font-sans text-caption text-muted-foreground",
className
)}
{...props}
/>
)
}
export {
Menu,
MenuContent,
MenuGroup,
MenuHeader,
MenuItem,
MenuSeparator,
MenuShortcut,
MenuTrigger,
}