Menu
A selector plate: items engraved into brushed metal, the highlighted one pressed in, a lamp for checked settings, engraved rules between groups and keycaps for shortcuts. Dropdown, submenu and context menu.
Copy page
The menu on a docs page with a Markdown version: copy it, view it, or open it in ChatGPT or Claude. Pass markdownUrl.
File actions
Shortcuts as keycaps, a submenu, a setting with a lamp, a sort order and a destructive item.
Preview on, sorted by modified.
Installation
Add with the CLI
Once per project, add the Bevel registry to
components.json.components.json { "registries": { "@bevel": "https://bevel-ui.vercel.app/r/{name}.json" } }Add Menu. This brings in the Bevel foundation, installs @base-ui/react and cn, and merges the styles into your global CSS.
Terminal npx shadcn@latest add @bevel/menu
Install manually
Install the dependencies.
Terminal pnpm add @base-ui/react cnAdd the Bevel foundation, if you haven't already. Light, depth, materials, motion and shape tokens, plus the utilities components use to apply them.
styles/bevel.css /* * Bevel foundation. Every component builds on these tokens and utilities; * component stylesheets only add their own palettes and shapes. * * Layers: * 1. Light one source, above and slightly in front of the screen. * 2. Depth named elevations built from the light: recessed, flush, * raised, pressed, object. * 3. Material a surface recipe (leather, plastic, metal) that reads * --bevel-material and --bevel-material-edge. * 4. Motion two curves, a spring, and durations named by purpose. * 5. Shape radii, stitching, and the type scale for amounts. */ :root { /* 1. Light */ --bevel-highlight: oklch(1 0 0 / 0.22); --bevel-highlight-strong: oklch(1 0 0 / 0.35); --bevel-shade: oklch(0 0 0 / 0.32); --bevel-shade-strong: oklch(0 0 0 / 0.5); --bevel-cast: oklch(0.25 0.03 50 / 0.22); /* Stamped into a surface: shaded on the upper inner edge, lit below. */ --bevel-deboss: 0 -1px 0 oklch(0 0 0 / 0.45), 0 1px 0 oklch(1 0 0 / 0.12); /* 3. Material defaults; components and variants override these. */ --bevel-material: oklch(0.47 0.085 52); --bevel-material-edge: oklch(0.3 0.06 44); --bevel-material-recess: oklch(0.2 0.008 260); /* neutral dark; materials set their own */ --bevel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .7 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"); --bevel-grain-size: 180px; /* 2. Depth */ --bevel-depth-recessed: inset 0 1px 2px var(--bevel-shade-strong), 0 1px 0 var(--bevel-highlight); --bevel-depth-flush: inset 0 1px 0 var(--bevel-highlight), inset 0 -1px 0 var(--bevel-shade); --bevel-depth-raised: inset 0 1px 0 var(--bevel-highlight), 0 1px 0 var(--bevel-material-edge), 0 2px 0 var(--bevel-material-edge), 0 3px 5px var(--bevel-shade); --bevel-depth-pressed: inset 0 1px 2px var(--bevel-shade), 0 1px 0 var(--bevel-highlight); /* A whole object resting on the page. */ --bevel-depth-object: inset 0 1px 0 var(--bevel-highlight), inset 0 -1px 0 var(--bevel-shade), 0 0 0 1px var(--bevel-material-edge), 0 1px 1px var(--bevel-cast), 0 6px 12px -2px var(--bevel-cast), 0 18px 32px -8px var(--bevel-cast); /* The shadow under a loose object, for filter: drop-shadow(). */ --bevel-depth-cast-sm: 0 4px 6px var(--bevel-cast); /* A thin object (a card) lying on another surface. */ --bevel-depth-sheet: inset 0 1px 0 var(--bevel-highlight-strong), inset 0 0 0 1px oklch(0 0 0 / 0.12), 0 -1px 2px oklch(0 0 0 / 0.18); /* 4. Motion */ --bevel-ease-out: cubic-bezier(0.23, 1, 0.32, 1); --bevel-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. */ --bevel-ease-spring: linear(0, 0.06, 0.202, 0.38, 0.561, 0.723, 0.856, 0.955, 1.023, 1.062, 1.081, 1.083, 1.075, 1.062, 1.047, 1.032, 1.019, 1.009, 1.002, 0.997, 0.994, 0.993, 0.993, 0.994, 1); --bevel-duration-press: 100ms; /* a control giving under a finger */ --bevel-duration-small: 200ms; /* hover lifts, tabs, small state changes */ --bevel-duration-move: 300ms; /* objects moving: cards, drawers */ --bevel-duration-ambient: 520ms; /* whole-scene changes: lighting */ --bevel-stagger: 30ms; --bevel-lift: 3px; /* how far an object rises on hover */ --bevel-perspective: 30rem; /* viewing distance for parts that turn in 3D (covers, flaps) */ /* Unchosen items in a group sit back in shadow. */ --bevel-dim: 0.82; /* 5. Shape */ --bevel-radius-xs: 0.25rem; --bevel-radius-sm: 0.5rem; --bevel-radius-md: 0.625rem; --bevel-radius-lg: 1rem; --bevel-radius-xl: 1.5rem; --bevel-stitch-width: 1.5px; --bevel-stitch-inset: 0.375rem; /* Amounts: the figure, its secondary parts (currency, cents), labels, fine print. */ --bevel-amount: 2.25rem; --bevel-amount-minor: 1.25rem; --bevel-label: 0.75rem; --bevel-fine: 0.6875rem; --bevel-tracking-label: 0.08em; /* stamped labels */ --bevel-tracking-print: 0.06em; /* printed marks on cards */ /* Paper and ink */ --bevel-paper: oklch(0.975 0.008 85); --bevel-paper-foreground: oklch(0.28 0.02 260); --bevel-paper-muted: oklch(0.48 0.015 260); --bevel-paper-rule: oklch(0.78 0.04 240 / 0.45); --bevel-paper-margin: oklch(0.68 0.14 25 / 0.45); --bevel-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9 .35' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E"); /* Ink soaks unevenly: mostly solid with pale specks. Used as a mask. */ --bevel-ink-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='i'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 2.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23i)'/%3E%3C/svg%3E"); --bevel-ink-red: oklch(0.52 0.19 27); --bevel-ink-blue: oklch(0.44 0.14 262); --bevel-ink-green: oklch(0.5 0.12 155); --bevel-ink-black: oklch(0.3 0.01 260); --bevel-ink-amber: oklch(0.55 0.13 65); /* Brass and ivory: instrument hardware and dial faces */ --bevel-brass: oklch(0.76 0.11 82); --bevel-brass-edge: oklch(0.52 0.08 70); --bevel-ivory: oklch(0.95 0.025 90); --bevel-ivory-foreground: oklch(0.25 0.015 60); --bevel-needle: oklch(0.55 0.2 27); /* Desk stationery: legal pads, pens, pencils, exhibit stickers */ --bevel-pad: oklch(0.95 0.075 98); /* yellow legal pad */ --bevel-pad-foreground: oklch(0.27 0.03 255); --bevel-pad-rule: oklch(0.72 0.06 235 / 0.45); --bevel-pad-margin: oklch(0.62 0.16 25 / 0.55); --bevel-pen-red: oklch(0.52 0.2 27); /* correcting pen */ --bevel-pen-blue: oklch(0.42 0.15 262); /* writing pen */ --bevel-pencil: oklch(0.55 0.01 260); /* not written in ink yet */ --bevel-sticker: oklch(0.9 0.12 95); /* exhibit sticker */ --bevel-sticker-foreground: oklch(0.26 0.03 70); /* Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. */ --bevel-font-marker: "Permanent Marker", "Marker Felt", "Chalkboard SE", ui-rounded, "Arial Rounded MT Bold", system-ui, sans-serif; } .dark { --bevel-highlight: oklch(1 0 0 / 0.14); /* Paper under dim light, not inverted: it's still paper. */ --bevel-paper: oklch(0.88 0.008 85); --bevel-ivory: oklch(0.86 0.022 90); --bevel-pad: oklch(0.86 0.07 98); --bevel-cast: oklch(0 0 0 / 0.55); } /* Depth */ @utility bevel-recessed { background-color: var(--bevel-material-recess); box-shadow: var(--bevel-depth-recessed); } @utility bevel-flush { box-shadow: var(--bevel-depth-flush); } @utility bevel-raised { background-color: color-mix(in oklch, var(--bevel-material), white 7%); background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 60%); box-shadow: var(--bevel-depth-raised); } @utility bevel-pressed { background-color: color-mix(in oklch, var(--bevel-material), black 4%); background-image: none; box-shadow: var(--bevel-depth-pressed); } @utility bevel-object { box-shadow: var(--bevel-depth-object); } /* Materials */ @utility bevel-leather { background-color: var(--bevel-material); background-image: var(--bevel-grain), linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade)); background-size: var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%; background-blend-mode: soft-light, normal; } @utility bevel-plastic { background-color: var(--bevel-material); background-image: linear-gradient(115deg, var(--bevel-highlight-strong), transparent 40%), linear-gradient(to bottom, transparent 25%, oklch(0 0 0 / 0.45)); box-shadow: var(--bevel-depth-sheet); } @utility bevel-metal { background-color: var(--bevel-material); background-image: linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade) 100%), repeating-linear-gradient(90deg, oklch(1 0 0 / 0.06) 0 1px, transparent 1px 3px); box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.25), inset 0 1px 0 var(--bevel-highlight-strong); } /* Details */ @utility bevel-deboss { text-shadow: var(--bevel-deboss); } @utility bevel-stitch { border-color: var(--bevel-stitch, oklch(0.88 0.05 82 / 0.8)); border-style: dashed; border-width: var(--bevel-stitch-width); filter: drop-shadow(0 1px 0 var(--bevel-shade)); } /* A lit fill sitting in a recess (meters, progress). Colour from --bevel-fill. */ @utility bevel-fill { background-color: var(--bevel-fill); background-image: linear-gradient(to bottom, oklch(1 0 0 / 0.55), transparent 55%, oklch(0 0 0 / 0.18)); box-shadow: 0 0 6px -1px var(--bevel-fill); } /* A small indicator light. Colour from --bevel-led. */ @utility bevel-led { background-color: var(--bevel-led); background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / 0.85), transparent 45%); box-shadow: 0 0 6px var(--bevel-led), inset 0 -1px 1px oklch(0 0 0 / 0.35), 0 1px 0 var(--bevel-highlight); } /* Paper: a sheet resting on the surface, with fibre in it. */ @utility bevel-paper { background-color: var(--bevel-paper); background-image: var(--bevel-fibre); background-size: 240px 240px; color: var(--bevel-paper-foreground); box-shadow: 0 1px 1px var(--bevel-cast), 0 6px 14px -4px var(--bevel-cast); } /* Ink on paper: multiplies into the surface and soaks unevenly. Colour from --bevel-ink. */ @utility bevel-ink { color: var(--bevel-ink, var(--bevel-ink-red)); border-color: currentColor; mix-blend-mode: multiply; mask-image: var(--bevel-ink-texture); mask-size: 160px 160px; } /* Glass over an instrument window: one reflection from the light above. Put on an overlay element. */ @utility bevel-glass { pointer-events: none; background-image: linear-gradient(165deg, var(--bevel-highlight-strong) 0%, transparent 38%, transparent 70%, var(--bevel-shade) 100%); } /* Material families: one per screen. Set on the screen's container. */ [data-bevel-family="leather"] { --bevel-cast: oklch(0.25 0.03 50 / 0.22); } [data-bevel-family="paper"] { --bevel-cast: oklch(0.3 0.01 250 / 0.16); } .dark :is([data-bevel-family="leather"], [data-bevel-family="paper"]) { --bevel-cast: oklch(0 0 0 / 0.55); } /* * Ruled paper: rules every --bevel-rule-height and a margin line at * --bevel-rule-margin. Set --bevel-sheet for the paper colour. The ruling * scrolls with the content. */ @utility bevel-ruled { background-color: var(--bevel-sheet, var(--bevel-paper)); background-image: linear-gradient( to right, transparent calc(var(--bevel-rule-margin) - 1px), var(--bevel-sheet-margin, var(--bevel-paper-margin)) calc(var(--bevel-rule-margin) - 1px) calc(var(--bevel-rule-margin) + 1px), transparent calc(var(--bevel-rule-margin) + 1px) ), repeating-linear-gradient( to bottom, transparent 0 calc(var(--bevel-rule-height) - 1px), var(--bevel-sheet-rule, var(--bevel-paper-rule)) calc(var(--bevel-rule-height) - 1px) var(--bevel-rule-height) ), var(--bevel-fibre); background-size: 100% 100%, 100% 100%, 240px 240px; background-attachment: local; color: var(--bevel-paper-foreground); box-shadow: 0 1px 1px var(--bevel-cast), 0 6px 14px -4px var(--bevel-cast); }Copy
menu.tsxintocomponents/ui.components/menu.tsx "use client" import * as React from "react" import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu" import { Menu as MenuPrimitive } from "@base-ui/react/menu" import { cn } from "cn" /** A trigger and the plate it opens. */ function Menu(props: MenuPrimitive.Root.Props) { return <MenuPrimitive.Root data-slot="menu" {...props} /> } /** Opens the plate. Renders a button; pass `render` to use your own. */ function MenuTrigger(props: MenuPrimitive.Trigger.Props) { return <MenuPrimitive.Trigger data-slot="menu-trigger" {...props} /> } const plate = cn( "menu-plate flex max-h-(--available-height) min-w-(--menu-min-width) origin-(--transform-origin) flex-col overflow-y-auto overscroll-contain rounded-(--bevel-radius-md) p-1 text-sm outline-none", "transition-[translate,opacity] duration-(--bevel-duration-small) ease-(--bevel-ease-out) motion-reduce:transition-none", "data-starting-style:opacity-0 data-ending-style:opacity-0", "data-[side=bottom]:data-starting-style:-translate-y-(--menu-lift) data-[side=top]:data-starting-style:translate-y-(--menu-lift)", "data-[side=right]:data-starting-style:-translate-x-(--menu-lift) data-[side=left]:data-starting-style:translate-x-(--menu-lift)", "forced-colors:border forced-colors:border-[CanvasText]" ) type MenuContentProps = MenuPrimitive.Popup.Props & Pick<MenuPrimitive.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset" | "collisionPadding"> /** The plate: brushed metal, items engraved into it. Drops a short way from the trigger. */ function MenuContent({ className, side = "bottom", align = "start", sideOffset = 6, alignOffset = 0, collisionPadding = 8, ...props }: MenuContentProps) { return ( <MenuPrimitive.Portal> <MenuPrimitive.Positioner side={side} align={align} sideOffset={sideOffset} alignOffset={alignOffset} collisionPadding={collisionPadding} className="z-50 outline-none" > <MenuPrimitive.Popup data-slot="menu-content" className={cn(plate, className)} {...props} /> </MenuPrimitive.Positioner> </MenuPrimitive.Portal> ) } /* The highlighted item sits pressed into the plate. */ const item = cn( "relative flex min-h-8 cursor-default items-center gap-2 rounded-(--bevel-radius-sm) px-2 py-1.5 outline-none select-none", "data-highlighted:bevel-pressed data-highlighted:[text-shadow:none]", "data-disabled:pointer-events-none data-disabled:opacity-50", "data-[variant=destructive]:text-(--menu-danger)", "forced-colors:data-highlighted:outline forced-colors:data-highlighted:outline-[Highlight]", "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-(--menu-ink-muted) data-[variant=destructive]:[&_svg]:text-current" ) type MenuItemProps = MenuPrimitive.Item.Props & { /** "destructive" engraves the item in red, for delete and the like. */ variant?: "default" | "destructive" } function MenuItem({ className, variant = "default", ...props }: MenuItemProps) { return <MenuPrimitive.Item data-slot="menu-item" data-variant={variant} className={cn(item, className)} {...props} /> } /** An item that navigates: a real link, so it opens in a new tab and shows its address. */ function MenuLinkItem({ className, ...props }: MenuPrimitive.LinkItem.Props) { return <MenuPrimitive.LinkItem data-slot="menu-link-item" className={cn(item, className)} {...props} /> } /** A small lamp: lit when its item is checked. The pill is a checkbox, the round one a radio. */ function MenuLamp({ shape }: { shape: "pill" | "round" }) { return ( <span aria-hidden data-slot="menu-lamp" className={cn( "bevel-led shrink-0 forced-color-adjust-none forced-colors:border forced-colors:border-[CanvasText] in-data-checked:forced-colors:bg-[Highlight]", shape === "pill" ? "h-1.5 w-2.5 rounded-full" : "size-2 rounded-full" )} /> ) } /** A setting you turn on and off. A lamp on the left lights when it's on. */ function MenuCheckboxItem({ className, children, ...props }: MenuPrimitive.CheckboxItem.Props) { return ( <MenuPrimitive.CheckboxItem data-slot="menu-checkbox-item" closeOnClick={false} className={cn(item, "pl-2.5", className)} {...props} > <MenuLamp shape="pill" /> {children} </MenuPrimitive.CheckboxItem> ) } function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) { return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} /> } /** One of a set. The chosen one's lamp is lit. */ function MenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) { return ( <MenuPrimitive.RadioItem data-slot="menu-radio-item" closeOnClick={false} className={cn(item, "pl-2.5", className)} {...props} > <MenuLamp shape="round" /> {children} </MenuPrimitive.RadioItem> ) } function MenuGroup(props: MenuPrimitive.Group.Props) { return <MenuPrimitive.Group data-slot="menu-group" {...props} /> } /** A small stamped heading over a group. */ function MenuGroupLabel({ className, ...props }: MenuPrimitive.GroupLabel.Props) { return ( <MenuPrimitive.GroupLabel data-slot="menu-group-label" className={cn( "px-2 pt-2 pb-1 text-(length:--bevel-fine) font-semibold tracking-(--bevel-tracking-label) text-(--menu-ink-muted) uppercase", className )} {...props} /> ) } /** An engraved rule between groups. */ function MenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) { return <MenuPrimitive.Separator data-slot="menu-separator" className={cn("menu-rule -mx-1 my-1", className)} {...props} /> } type MenuShortcutProps = Omit<React.ComponentProps<"span">, "children"> & { /** One keycap per key, e.g. ["⌘", "C"]. Also set aria-keyshortcuts on the item. */ keys: string[] } /** The item's keyboard shortcut, as small keycaps at the end of the row. Hidden from screen readers. */ function MenuShortcut({ className, keys, ...props }: MenuShortcutProps) { return ( <span aria-hidden data-slot="menu-shortcut" className={cn("ml-auto flex shrink-0 items-center gap-0.5 pl-4 [text-shadow:none]", className)} {...props} > {keys.map((key, i) => ( <kbd key={i} data-slot="menu-key" className="bevel-raised inline-flex h-(--menu-key-size) min-w-(--menu-key-size) -translate-y-px items-center justify-center rounded-(--bevel-radius-xs) px-1 font-sans text-(length:--bevel-fine) leading-none font-medium text-(--menu-key-foreground)" > {key} </kbd> ))} </span> ) } /** Holds a submenu: its trigger item and its own plate. */ function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) { return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} /> } /** The item that opens a submenu, with a chevron. Opens on hover, → or Enter. */ function MenuSubTrigger({ className, children, ...props }: MenuPrimitive.SubmenuTrigger.Props) { return ( <MenuPrimitive.SubmenuTrigger data-slot="menu-sub-trigger" className={cn(item, "data-popup-open:bevel-pressed data-popup-open:[text-shadow:none]", className)} {...props} > {children} <svg aria-hidden viewBox="0 0 16 16" className="ml-auto" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"> <path d="M6 4l4 4-4 4" /> </svg> </MenuPrimitive.SubmenuTrigger> ) } /** A submenu's plate. Opens beside its trigger. */ function MenuSubContent({ side = "right", align = "start", sideOffset = 2, alignOffset = -4, ...props }: MenuContentProps) { return <MenuContent side={side} align={align} sideOffset={sideOffset} alignOffset={alignOffset} {...props} /> } /** Right-click (or long-press) an area to open a plate at the pointer. */ function ContextMenu(props: ContextMenuPrimitive.Root.Props) { return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} /> } /** The area that opens the context menu. Renders a div. */ function ContextMenuTrigger(props: ContextMenuPrimitive.Trigger.Props) { return <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} /> } /** The context menu's plate, at the pointer. Fill it with the same Menu items. */ function ContextMenuContent({ className, ...props }: MenuPrimitive.Popup.Props & Pick<ContextMenuPrimitive.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset">) { const { side, align, sideOffset, alignOffset, ...popup } = props return ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Positioner side={side} align={align} sideOffset={sideOffset} alignOffset={alignOffset} collisionPadding={8} className="z-50 outline-none" > <ContextMenuPrimitive.Popup data-slot="menu-content" className={cn(plate, className)} {...popup} /> </ContextMenuPrimitive.Positioner> </ContextMenuPrimitive.Portal> ) } export { Menu, MenuTrigger, MenuContent, MenuItem, MenuLinkItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuGroup, MenuGroupLabel, MenuSeparator, MenuShortcut, MenuSub, MenuSubTrigger, MenuSubContent, ContextMenu, ContextMenuTrigger, ContextMenuContent, } export type { MenuContentProps, MenuItemProps, MenuShortcutProps }Copy
menu.cssintoyour styles.styles/menu.css /* * Menu: a selector plate. Items are engraved into brushed metal; the * highlighted one sits pressed in, checked items light a lamp, separators * are engraved rules and shortcuts are small keycaps. Builds on bevel.css * (light, depth, materials, motion, shape). */ :root { --menu-plate: oklch(0.91 0.004 250); --menu-plate-edge: oklch(0.68 0.006 250); --menu-rim: oklch(0 0 0 / 0.14); --menu-brush: oklch(1 0 0 / 0.05); /* brushing marks across the plate */ --menu-ink: oklch(0.25 0.01 260); --menu-ink-muted: oklch(0.45 0.01 260); /* The light catches the lower lip of each engraved cut. */ --menu-engrave: 0 1px 0 oklch(1 0 0 / 0.55); --menu-rule-shade: oklch(0 0 0 / 0.16); --menu-rule-light: oklch(1 0 0 / 0.7); --menu-lamp-off: oklch(0.6 0.02 150); --menu-lamp-on: oklch(0.74 0.19 145); --menu-key: oklch(0.98 0.003 90); --menu-key-edge: oklch(0.74 0.006 250); --menu-key-foreground: oklch(0.32 0.01 260); --menu-danger: oklch(0.5 0.19 27); --menu-depth: inset 0 1px 0 var(--bevel-highlight-strong), inset 0 0 0 1px var(--menu-rim), 0 1px 1px var(--bevel-cast), 0 10px 22px -8px var(--bevel-cast); --menu-min-width: 12rem; --menu-key-size: 1.125rem; --menu-lift: 0.25rem; } .dark { --menu-plate: oklch(0.32 0.006 260); --menu-plate-edge: oklch(0.2 0.006 260); --menu-rim: oklch(0 0 0 / 0.4); --menu-ink: oklch(0.94 0.004 90); --menu-ink-muted: oklch(0.72 0.008 260); --menu-engrave: 0 -1px 0 oklch(0 0 0 / 0.5); --menu-rule-shade: oklch(0 0 0 / 0.45); --menu-rule-light: oklch(1 0 0 / 0.08); --menu-lamp-off: oklch(0.38 0.02 150); --menu-key: oklch(0.42 0.006 260); --menu-key-edge: oklch(0.22 0.006 260); --menu-key-foreground: oklch(0.92 0.004 90); --menu-danger: oklch(0.72 0.17 27); } [data-slot="menu-content"] { --bevel-material: var(--menu-plate); --bevel-material-edge: var(--menu-plate-edge); } [data-slot="menu-shortcut"] { --bevel-material: var(--menu-key); --bevel-material-edge: var(--menu-key-edge); } [data-slot="menu-lamp"] { --bevel-led: var(--menu-lamp-off); } [data-checked] > [data-slot="menu-lamp"] { --bevel-led: var(--menu-lamp-on); } /* Brushed metal plate, lifted off the page. */ @utility menu-plate { background-color: var(--menu-plate); background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 40%, var(--bevel-shade) 140%), repeating-linear-gradient(90deg, var(--menu-brush) 0 1px, transparent 1px 3px); box-shadow: var(--menu-depth); color: var(--menu-ink); text-shadow: var(--menu-engrave); } /* An engraved rule: a shaded cut with a lit lip below it. */ @utility menu-rule { border-top: 1px solid var(--menu-rule-shade); border-bottom: 1px solid var(--menu-rule-light); }Import the stylesheets after Tailwind in your global CSS, foundation first.
globals.css @import "./bevel.css"; @import "./menu.css";
Anatomy
Each part is its own component. Leave out the ones you don't need.
<Menu>
<MenuTrigger />
<MenuContent>
<MenuItem>
Duplicate
<MenuShortcut keys={["⌘", "D"]} />
</MenuItem>
<MenuLinkItem href="/page.md" />
<MenuSub>
<MenuSubTrigger />
<MenuSubContent>
<MenuItem />
</MenuSubContent>
</MenuSub>
<MenuSeparator />
<MenuCheckboxItem />
<MenuGroup>
<MenuGroupLabel />
<MenuRadioGroup>
<MenuRadioItem />
</MenuRadioGroup>
</MenuGroup>
</MenuContent>
</Menu>Parts
| Part | Renders | What it does |
|---|---|---|
| MenuMenu root | Menu root | open, defaultOpen, onOpenChange, modal. |
| MenuTriggerbutton | button | Opens the plate. Pass render to use your own button. |
| MenuContentMenu popup | Menu popup | The plate. side (default bottom), align (default start), sideOffset, alignOffset, collisionPadding. Scrolls when tall. |
| MenuItemdiv (menuitem) | div (menuitem) | Pressed in while highlighted. onClick runs it. variant="destructive" engraves it in red. Icons are muted and sized for you. |
| MenuLinkItema (menuitem) | a (menuitem) | An item that is a real link: href, target. |
| MenuCheckboxItemdiv (menuitemcheckbox) | div (menuitemcheckbox) | A pill lamp, lit when checked. checked, onCheckedChange. Stays open when toggled. |
| MenuRadioGroupdiv (group) | div (group) | value, onValueChange. |
| MenuRadioItemdiv (menuitemradio) | div (menuitemradio) | A round lamp, lit when chosen. value. |
| MenuGroupdiv (group) | div (group) | Groups items under a label. |
| MenuGroupLabeldiv | div | A small stamped heading. |
| MenuSeparatordiv (separator) | div (separator) | An engraved rule. |
| MenuShortcutspan | span | keys: one keycap per key. Hidden from screen readers; put aria-keyshortcuts on the item. |
| MenuSubSubmenu root | Submenu root | Holds a submenu. |
| MenuSubTriggerdiv (menuitem) | div (menuitem) | Opens the submenu on hover, → or Enter. Has a chevron. |
| MenuSubContentMenu popup | Menu popup | The submenu's plate, beside its trigger. |
| ContextMenuContext menu root | Context menu root | Opens on right-click or long-press. |
| ContextMenuTriggerdiv | div | The area that opens it. |
| ContextMenuContentMenu popup | Menu popup | The plate at the pointer. Fill it with the same items. |