V2 componentsUpdated Oct 14, 2026
Menu
A dropdown of actions with full keyboard navigation. The trigger is styled as a Button; Content wraps portal, positioner, and popup.
Source: packages/ui/src/components/menu
import { Menu } from '@highlight/ui'Change the props; the preview and the snippet update together.
<Menu> <Menu.Trigger>Actions</Menu.Trigger> <Menu.Content sideOffset={8}> <Menu.Item>Rename…</Menu.Item> <Menu.Item>Duplicate</Menu.Item> <Menu.Item disabled>Move to team (soon)</Menu.Item> <Menu.Separator /> <Menu.Item variant="danger">Delete</Menu.Item> </Menu.Content></Menu>Items, a separator, and a destructive action last.
import { Menu } from '@highlight/ui' export default function MenuBasic() { return ( <Menu> <Menu.Trigger>Actions</Menu.Trigger> <Menu.Content> <Menu.Item>Rename…</Menu.Item> <Menu.Item>Duplicate</Menu.Item> <Menu.Item disabled>Move to team (soon)</Menu.Item> <Menu.Separator /> <Menu.Item variant="danger">Delete</Menu.Item> </Menu.Content> </Menu> )}import { useState } from 'react'import { Menu } from '@highlight/ui' export default function MenuSelection() { const [showTimestamps, setShowTimestamps] = useState(true) const [showSpeakers, setShowSpeakers] = useState(false) const [density, setDensity] = useState('comfortable') return ( <Menu> <Menu.Trigger variant="outline">View</Menu.Trigger> <Menu.Content> <Menu.Group> <Menu.GroupLabel>Show</Menu.GroupLabel> <Menu.CheckboxItem checked={showTimestamps} onCheckedChange={setShowTimestamps}> <Menu.CheckboxItemIndicator /> Timestamps </Menu.CheckboxItem> <Menu.CheckboxItem checked={showSpeakers} onCheckedChange={setShowSpeakers}> <Menu.CheckboxItemIndicator /> Speaker names </Menu.CheckboxItem> </Menu.Group> <Menu.Separator /> <Menu.Group> <Menu.GroupLabel>Density</Menu.GroupLabel> <Menu.RadioGroup value={density} onValueChange={setDensity}> <Menu.RadioItem value="comfortable"> <Menu.RadioItemIndicator /> Comfortable </Menu.RadioItem> <Menu.RadioItem value="compact"> <Menu.RadioItemIndicator /> Compact </Menu.RadioItem> </Menu.RadioGroup> </Menu.Group> </Menu.Content> </Menu> )}import { Menu } from '@highlight/ui' export default function MenuItemVariants() { return ( <Menu> <Menu.Trigger variant="outline">Item variants</Menu.Trigger> <Menu.Content> <Menu.Item>Default</Menu.Item> <Menu.Item variant="primary">Primary</Menu.Item> <Menu.Item variant="bold">Bold</Menu.Item> <Menu.Item variant="success">Success</Menu.Item> <Menu.Item variant="danger">Danger</Menu.Item> </Menu.Content> </Menu> )}Icons are 14px at the system stroke, outline only: never mix filled glyphs into a list. Once any row has a leading icon, rows without one indent so every label starts on one edge. Menu.ItemText takes the title as children and an optional description, a second line in secondary ink that wraps while the title truncates; the row grows and the icon and check stay on the title’s line. Keep descriptions to one short sentence, and use them on every row of a group or none.
import { Copy, Link, Mail, MessageSquare } from 'lucide-react'import { Menu } from '@highlight/ui' export default function MenuDescriptions() { return ( <Menu> <Menu.Trigger variant="outline">Share</Menu.Trigger> <Menu.Content> <Menu.Item> <Link /> <Menu.ItemText description="Anyone with the link can read the summary.">Copy link</Menu.ItemText> </Menu.Item> <Menu.Item> <MessageSquare /> <Menu.ItemText description="Posts the summary and action items to a channel.">Send to Slack</Menu.ItemText> </Menu.Item> <Menu.Item> <Mail /> <Menu.ItemText description="Emails every attendee a recap.">Email attendees</Menu.ItemText> </Menu.Item> <Menu.Separator /> <Menu.Item> <Menu.ItemText>Export as Markdown</Menu.ItemText> </Menu.Item> <Menu.Item> <Copy /> <Menu.ItemText>Duplicate</Menu.ItemText> </Menu.Item> </Menu.Content> </Menu> )}Menu.ItemLead is a 20px lane for something that is not an icon: a workspace tile, a person’s face. Put it first in the row. Once a list has one, every label starts after the 20px lane: bare rows indent to it and icon rows centre their 14px glyph in it, so leads and icons can share a list. Menu.Static has the row’s geometry but states rather than acts: not a menuitem, no keyboard stop, no hover.
import { Menu } from '@highlight/ui'import { LogOut, Plus } from 'lucide-react' function Face({ children }: { children: string }) { return ( <span className="bg-element text-foreground-secondary flex size-5 items-center justify-center rounded-full text-[10px] font-medium"> {children} </span> )} function Tile({ children }: { children: string }) { return ( <span className="bg-brand text-on-brand flex size-5 items-center justify-center rounded-[6px] text-[10px] font-medium"> {children} </span> )} export default function MenuLeadAndStatic() { return ( <Menu> <Menu.Trigger variant="outline">Account</Menu.Trigger> <Menu.Content> <Menu.Static> <Menu.ItemLead> <Face>JD</Face> </Menu.ItemLead> <Menu.ItemText description="jane@acme.com">Signed in as Jane</Menu.ItemText> </Menu.Static> <Menu.Separator /> <Menu.Group> <Menu.GroupLabel>Workspaces</Menu.GroupLabel> <Menu.Item> <Menu.ItemLead> <Tile>A</Tile> </Menu.ItemLead> <Menu.ItemText>Acme</Menu.ItemText> </Menu.Item> <Menu.Item> <Menu.ItemLead> <Tile>P</Tile> </Menu.ItemLead> <Menu.ItemText>Personal</Menu.ItemText> </Menu.Item> <Menu.Item> <Plus /> <Menu.ItemText>Create workspace…</Menu.ItemText> </Menu.Item> </Menu.Group> <Menu.Separator /> <Menu.Item> <Menu.ItemText>Settings</Menu.ItemText> </Menu.Item> <Menu.Item> <LogOut /> <Menu.ItemText>Sign out</Menu.ItemText> </Menu.Item> </Menu.Content> </Menu> )}Wrap a disabled Menu.Item in a Tooltip.Trigger rendered as a div, so the wrapper takes the pointer a disabled row gives up. Base UI keeps a disabled item focusable, so keyboard users reach the reason too.
import { Menu, Tooltip } from '@highlight/ui' export default function MenuDisabledReason() { return ( <Menu> <Menu.Trigger variant="outline">Meeting</Menu.Trigger> <Menu.Content> <Menu.Item> <Menu.ItemText>Copy link</Menu.ItemText> </Menu.Item> <Tooltip> <Tooltip.Trigger render={<div />}> <Menu.Item disabled> <Menu.ItemText>Delete recording</Menu.ItemText> </Menu.Item> </Tooltip.Trigger> <Tooltip.Content side="right">Only the organizer can delete this recording.</Tooltip.Content> </Tooltip> </Menu.Content> </Menu> )}The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.
Menu- Base UI Menu.Root. Owns open state; renders nothing itself.
Menu.Trigger- Button-styled; variant, tone, size, shape.
Menu.Content- side, sideOffset, align, alignOffset, finalFocus.
Menu.Item / LinkItem- variant: 'default' | 'primary' | 'bold' | 'success' | 'danger'.
Menu.CheckboxItem / CheckboxItemIndicator- checked, onCheckedChange. The indicator trails the label and defaults to a check, the same mark Select uses.
Menu.RadioGroup / RadioItem / RadioItemIndicator- value, onValueChange. Same trailing check as CheckboxItem.
Menu.ItemText- The row’s text: title as children, optional description under it. Shared with ContextMenu, Select and Combobox.
Menu.ItemLead- A 20px leading lane for a tile or face in place of an icon. First child of the row.
Menu.Static- A non-interactive row with Item’s geometry. Not a menuitem.
Menu.Group / GroupLabel / Separator- Structure.
Menu.SubmenuRoot / SubmenuTrigger- Nest a Content inside SubmenuRoot.