Menu Item
Menu-styled rows that live on the page instead of in a popup — settings lists, account panels, sheet bodies and sidebars of actions. Plain or grouped cards, with icons, avatars, values, badges, shortcuts, checkbox, switch and radio rows.
pnpm add @tessinaui/uiWhen to use
- Rows that live on the page. A settings screen, an account panel, or a list of actions inside a sheet or card. Each row is its own Tab stop; the rows are the ones Dropdown Menu paints.
- Settings that take effect at once.
MenuItemSwitchfor on and off,MenuItemCheckboxfor several,MenuItemRadioGroupfor one of many. Usevariant="grouped"for the settings-screen layout. - Labels that name the result. Start an action with a verb ("Export", "Invite people") and a destination with a noun ("Billing"). Sentence case, no articles, no trailing period. End a label with "…" when the action needs more input before it happens ("Rename…").
intent="error"for an action that cannot be undone, placed last. Reversible actions, such as archive or remove from favourites, stay neutral. So does a link to a destructive page, such as "Delete account" in a settings nav: it is a destination, not the action.- One icon rule per group. Give every row in a group a leading icon, or none. When rows mix, use
insetso the labels share one edge. - Hide what is never available; disable what will be. Hide an action the person will never be allowed to take. Disable one that will become available later: a disabled action row stays focusable so it can still be found. A checkbox, switch or radio row follows its control, so
disabledtakes it out of the Tab order; to keep one findable, make itreadOnlyand say why in itsdescription.
When not to use
- For a popup that opens from a trigger or a right-click. Use DropdownMenu or ContextMenu.
- For a command palette or a choice from a list. Use Command or Select.
- For a mobile bottom sheet of actions. Use ActionSheet, which has its own tile and cancel layouts.
- For the app-shell navigation rail. Use Sidebar, which adds collapse and tooltips.
- For a link, button or input inside a row's label or description. The row is already a button. Use
actionfor a second control. - For nesting by indentation. Do not nest more than one level; use an expandable row or a new screen.
Installation
pnpm add @tessinaui/uiUsage
import {
MenuItemList,
MenuItemGroup,
MenuItem,
MenuItemSwitch,
} from "@tessinaui/ui/menu-item";<MenuItemList variant="grouped">
<MenuItemGroup label="General">
<MenuItem leadingIcon={<Palette />} meta="System" chevron>
Appearance
</MenuItem>
<MenuItemSwitch leadingIcon={<Bell />} checked={on} onCheckedChange={setOn}>
Notifications
</MenuItemSwitch>
</MenuItemGroup>
<MenuItemGroup>
<MenuItem intent="error" leadingIcon={<LogOut />}>Sign out</MenuItem>
</MenuItemGroup>
</MenuItemList>The rows are the same rows Dropdown Menu paints — same sizes, radius steps, inks and destructive treatment — but each one is its own Tab stop on the page. A popup menu stays DropdownMenu.
Choosing between look-alikes
MenuItem— rows on the page: a settings screen, an account panel, a list of actions inside a sheet or card.DropdownMenu/ContextMenu— the same rows in a popup that opens from a trigger or a right-click.ActionSheet— a mobile bottom sheet of actions, with its own tile and cancel layouts.Sidebar— the app-shell navigation rail, with collapse and tooltips.
Examples
Default
A list of actions with leading icons, a separator and a destructive sign-out, next to a text-only list.
Sizes
Five sizes from xs to xl. On a touch pointer every size keeps a 44px row.
Rounded
rounded sets the row radius — the active row's fill shows the corner. Content inside a row can follow it with rounded-[var(--menu-item-radius)].
Leading media
Avatars, logos, tinted tiles and a coloured glyph in the leading slot. menuItemAvatarSize picks the avatar that fits each row size, and inset lines plain rows up with a checkbox column.
Trailing content
A current value (meta) with a drill-in chevron, a count as a pill or a bare number, a trailing icon, and a shortcut beside a chevron.
Badges and shortcuts
badge sits right after the label; a badge passed as meta sits at the trailing edge. Shortcuts take canonical key names that resolve per platform.
Description and wrapping
Descriptions go under the label or beside it with descriptionPlacement="inline". Long content wraps by default, so nothing is cut off on a narrow screen. wrap={false} cuts it to one line with nothing to show the rest — keep it for text that is readable somewhere else too.
Destructive
intent="error" paints the row red and adds "destructive action" to what a screen reader hears. Put it last, after a separator or in its own group; destructiveHint rewords the hint. An active destructive row fills red at rest instead of taking the neutral active fill, so its red label never sits on grey.
States
Hover, keyboard focus and press; a disabled row that stays discoverable; and a loading row whose spinner replaces its icon.
Links and the current page
render={<a href />} makes the row a real link. active marks the current page with aria-current="page", and target="_blank" adds the outbound glyph and "(opens in a new tab)".
Checkbox rows
MenuItemCheckbox for multi-select, including a mixed "Show all" parent.
Radio rows
MenuItemRadioGroup for a single choice — a tick on the chosen row, or a radio on every row with indicator="radio". The group is one Tab stop and the arrow keys select.
Switch rows
MenuItemSwitch for settings that take effect at once.
Groups and separators
A group heading names its list and can carry an action; a footnote sits under the group. Rules between rows are none, full or inset.
Grouped cards
variant="grouped" puts each group in its own card with inset rules — the settings-screen layout.
Row action
action puts a second control beside the row. It is always visible on touch; with a mouse it appears on hover or when anything in the row has focus.
Expandable row
A row that expands in place, with aria-expanded and aria-controls — the phone-friendly alternative to a submenu.
Skeleton
MenuItemListSkeleton stands in for rows that have not loaded, built from the same size and radius maps as the real rows.
RTL
Under dir="rtl" the padding, trailing columns and chevron all mirror.
On color
Inside a coloured <Surface> a plain list inherits tone="on-color": its rows, rules, headings and footnotes take the plate's ink and muted tier, a row hovers and presses to a hairline instead of a page wash, the current row is the inverse chip, and a destructive row keeps its meaning through the plate's error ink. Nothing needs to be set.
A grouped card is the menu popup's own surface, so it keeps its page look on any plate: the rows, switches, badges and keycaps inside it read the card, while the group headings and footnotes between the cards read the plate.
API Reference
MenuItemList
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Row density for every row inside |
rounded | "none" | "sm" | "md" | "lg" | "full" | — | Row radius, and a grouped card's radius. Unset, both follow size |
variant | "plain" | "grouped" | "plain" | Rows on the page, or each group in its own card |
separators | "none" | "full" | "inset" | "none" plain, "inset" grouped | Rules between rows |
destructiveHint | string | false | "destructive action" | What a screen reader adds after a destructive row's name |
MenuItemGroup
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Section heading; names the group's list |
action | ReactNode | — | A control beside the heading |
description | ReactNode | — | Footnote under the group |
listClassName | string | — | Classes for the group's own list |
MenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
leadingIcon | ReactNode | — | Icon, avatar, logo or tile before the label |
trailingIcon | ReactNode | — | Decorative icon at the end |
description | ReactNode | — | Secondary text |
descriptionPlacement | "block" | "inline" | "block" | Under the label or beside it |
wrap | boolean | true | Wrap long content; false truncates to one line |
badge | ReactNode | — | Status pill after the label |
meta | ReactNode | — | Trailing value or count |
shortcut | string | string[] | — | Keyboard hint; ["mod", "K"] resolves per platform |
chevron | boolean | false | Drill-in chevron, mirrored in RTL |
intent | "none" | "error" | "none" | error for a destructive action |
inset | boolean | false | Align the label with the checkbox column |
active | boolean | false | Current destination — fill + aria-current="page"; a destructive row fills red |
disabled | boolean | false | Unavailable; stays focusable |
loading | boolean | false | Busy; spinner replaces the leading icon |
action | ReactNode | — | A second control beside the row |
render | ReactElement | — | Replace the <button> — <a href>, a router link |
size / rounded | — | from the list | Per-row override |
MenuItemCheckbox · MenuItemSwitch
Every display prop of MenuItem except inset, plus the Base UI control's own: checked, defaultChecked, onCheckedChange, disabled, readOnly, name, and indeterminate on the checkbox.
MenuItemRadioGroup · MenuItemRadio
The group takes value, defaultValue, onValueChange, disabled and name; name it with aria-label or aria-labelledby. Each radio takes value, the display props, and indicator: "check" | "radio" (default "check").
MenuItemListSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
items | number | 4 | Row placeholders |
media | "none" | "icon" | "avatar" | "icon" | What leads each row |
showDescription | boolean | false | A second line per row |
showMeta | boolean | false | A trailing value placeholder |
size / rounded / variant | — | as MenuItemList | Match the list it stands in for |
label | string | "Loading", Arabic under dir="rtl" | Announced while the placeholder is up |
Accessibility
- Semantics are a plain list —
ul[role=list] > li > button(ora). There are nomenuroles, because there is no menu. - A group's heading names its list. A separator is decorative.
- A disabled action row keeps focus (
aria-disabled) so it can still be found. Checkbox, switch and radio rows are Base UI's own controls rendered as the row, so their roles,aria-checkedand keys come from the primitive —disabledtakes them out of the Tab order, as it does the standalone controls, andreadOnlykeeps one focusable while refusing the toggle.
Keyboard
| Key | Action |
|---|---|
Tab / Shift+Tab | Move between rows, row actions and radio groups |
Enter / Space | Activate the row, or toggle a checkbox or switch row |
ArrowDown / ArrowUp | Inside a radio group, move and select |