Skip to content
Highlight

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

tsx
import { Menu } from '@highlight/ui'

Change the props; the preview and the snippet update together.

Props
Trigger variant
Side
Align
8
Last item variant
Separator
Disabled item
tsx
<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.

tsx
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>  )}
tsx
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>  )}
tsx
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.

tsx
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.

tsx
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.

tsx
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.