Menu

Actions and destinations, opened from a trigger or a right click.

StableSince 1.0.0
On this page

Import

import {
  ContextMenu,
  Menu,
  ThemeMenuRadioGroup,
} from '@envsemble/envui';

Usage

Menu.Root opens from its trigger, such as a Button, an IconButton or an AvatarButton, which receives the menu button semantics and must accept native props and a ref, and names the menu, unless aria-label does. Its children are Menu.Header, Menu.Item, Menu.Group, Menu.RadioGroup, Menu.CheckboxItem, Menu.Submenu and Menu.Separator.

A Menu.Item runs its onClick and takes:

  • its label as children;
  • a decorative icon;
  • a muted description, read as its description;
  • a shortcut, such as Ctrl+D, shown as muted text in one unit and announced as aria-keyshortcuts;
  • end content, such as a Badge, which is part of its name; a count there shows as its figures alone, without a pill, apart from the shortcut's keys;
  • tone="danger" for a destructive action, such as Delete, whose label and icon take the danger color while its row highlights as any other's;
  • disabled, which keeps it in place with aria-disabled, its icon dimmed and its label in the disabled text color, apart from its description, which stays muted and readable to say why.
IconsAndShortcuts.tsx
import { useState } from 'react';
import { IconButton, Menu, Column, Text, Badge } from '@envsemble/envui';
import { IconComment } from '@envsemble/envui/icons/Comment';
import { IconDelete } from '@envsemble/envui/icons/Delete';
import { IconDownload } from '@envsemble/envui/icons/Download';
import { IconEdit } from '@envsemble/envui/icons/Edit';
import { IconFileCopy } from '@envsemble/envui/icons/FileCopy';
import { IconMoreVert } from '@envsemble/envui/icons/MoreVert';

export default function IconsAndShortcuts() {
  const [lastAction, setLastAction] = useState('');
  return (
    <Column gap={4} align="start">
      <Menu.Root
        aria-label="Report actions"
        align="start"
        trigger={<IconButton icon={<IconMoreVert />} aria-label="More actions" />}
      >
        <Menu.Item icon={<IconEdit />} shortcut="F2" onClick={() => setLastAction('Rename')}>
          Rename
        </Menu.Item>
        <Menu.Item
          icon={<IconFileCopy />}
          shortcut="Ctrl+D"
          onClick={() => setLastAction('Duplicate')}
        >
          Duplicate
        </Menu.Item>
        <Menu.Item
          icon={<IconComment />}
          end={<Badge count={3} aria-label="3 unresolved" />}
          onClick={() => setLastAction('Show comments')}
        >
          Show comments
        </Menu.Item>
        <Menu.Item
          icon={<IconDownload />}
          description="Available once the report is final"
          disabled
        >
          Download PDF
        </Menu.Item>
        <Menu.Separator />
        <Menu.Item
          icon={<IconDelete />}
          tone="danger"
          shortcut="Del"
          onClick={() => setLastAction('Delete')}
        >
          Delete
        </Menu.Item>
      </Menu.Root>
      <Text variant="body-sm" role="status">
        {lastAction && `Chose ${lastAction}`}
      </Text>
    </Column>
  );
}

Opening

  • side is bottom, the default, top, left, right, or inline-start and inline-end, which follow the text direction, and it flips where the window has no room.
  • align is start, center or end, the default.
  • Leave the menu uncontrolled, with defaultOpen to start open, or control it with open and onOpenChange, which receives why, such as escape-key or item-press.
  • className, ref, style and the other native props reach the popup.

Context menu

ContextMenu makes its one child the region, which must accept native props and a ref, and takes the menu's parts as content, as the ContextMenuForRows example does.

  • content can be a function that receives the element the menu opened on and returns them; one that returns null leaves the menu closed there.
  • disabled keeps the browser's own context menu.
  • open, onOpenChange and anchorPoint open it from another control at a point.
  • Offer every action as a visible control too.
<ContextMenu
  aria-label="Document actions"
  content={(target) => {
    const id = target?.closest<HTMLElement>('[data-entry]')?.dataset.entry;
    return id ? <Menu.Item onClick={() => remove(id)}>Delete</Menu.Item> : null;
  }}
>
  <List.Root aria-label="Documents">{rows}</List.Root>
</ContextMenu>

Account menu

Menu.Header holds the context at the top, such as the signed-in person's avatar, name and e-mail address, under an AvatarButton trigger, as the account and organizations pattern builds it. The avatar sits at the end of a top bar, so the menu lines up with the trigger's end.

<Menu.Root
  aria-label="Account"
  trigger={<AvatarButton name="Alex Morgan" aria-label="Open account menu" />}
>
  <Menu.Header>
    <Column align="center" gap={1} padding={2}>
      <Avatar name="Alex Morgan" size="lg" />
      <Text>Alex Morgan</Text>
      <Text variant="body-sm" emphasis="muted">
        alex@example.com
      </Text>
    </Column>
  </Menu.Header>
  <Menu.Item href="/account" icon={<IconManageAccounts />}>
    Account settings
  </Menu.Item>
  <Menu.Separator />
  <Menu.Item icon={<IconLogout />}>Sign out</Menu.Item>
</Menu.Root>

Groups, choices and submenus

  • Menu.Group puts items under its label; Menu.Separator splits sets without one.
  • Menu.RadioGroup holds Menu.RadioItems, each with a value, and takes value and onValueChange, or defaultValue alone, and a label, or aria-labelledby; a radio item closes the menu unless closeOnClick={false}.
  • Menu.CheckboxItem takes checked and onCheckedChange, or defaultChecked alone, and keeps the menu open unless closeOnClick.
  • ThemeMenuRadioGroup is the theme choice as radio items, from the nearest ThemePreferenceProvider, or from value and onValueChange; it keeps the menu open so people see the theme change.
  • Menu.Submenu is a row with its label that opens its children beside it; open, defaultOpen and onOpenChange suit loading its items as it opens.
<Menu.Root trigger={<Button variant="secondary">View</Button>}>
  <Menu.RadioGroup label="Sort by" value={sort} onValueChange={setSort}>
    <Menu.RadioItem value="name">Name</Menu.RadioItem>
    <Menu.RadioItem value="size">Size</Menu.RadioItem>
  </Menu.RadioGroup>
  <Menu.Group label="Show">
    <Menu.CheckboxItem checked={hiddenFiles} onCheckedChange={setHiddenFiles}>
      Hidden files
    </Menu.CheckboxItem>
  </Menu.Group>
  <Menu.Submenu label="Theme">
    <ThemeMenuRadioGroup />
  </Menu.Submenu>
</Menu.Root>

Destinations

With href, a Menu.Item is a link, through the link component of LinkProvider, and the menu closes as it is followed.

  • Its onClick runs as it is followed.
  • target and rel are a native link's.
  • A disabled one renders no link.
  • aria-current="page" marks the page shown now.
<Menu.Item href="/help/guide" icon={<IconMenuBook />}>
  User guide
</Menu.Item>

Composition

The parts:

  • The popup, env-menu, and its positioner, env-menu-positioner; the header, env-menu-header.
  • A group, env-menu-group, with its label, env-menu-group-label.
  • Each item, env-menu-item, with data-highlighted, data-disabled and data-tone. Its label and description are in env-menu-item-text, env-menu-item-label and env-menu-item-description; its end, env-menu-item-end, holds the shortcut, env-menu-item-shortcut.
  • A radio or checkbox item, env-menu-choice, with data-checked, and its check in env-menu-item-indicator-slot and env-menu-item-indicator.
  • A submenu's row, env-menu-submenu-trigger, with its chevron, env-menu-submenu-chevron.
  • The separator, env-menu-separator.

Customize

Its popup is frosted, as every popup is: --env-popup-opacity, --env-popup-blur and --env-popup-shadow, set on a theme, change it, as Popover describes.

Accessibility

You provide

  • An aria-label for every ContextMenu and every icon-only trigger; a Menu.Root takes its trigger's name, unless its own aria-label says it better.
  • A visible control for every action of a context menu.
  • Focus for after an action removes the trigger, such as on the next row.

envUI provides

  • The menu button pattern: aria-haspopup and aria-expanded on the trigger, and menuitem, menuitemradio and menuitemcheckbox rows.
  • Descriptions read as descriptions, and shortcuts as aria-keyshortcuts.
  • Focus that moves into the menu, roves between items and returns on close.

Props

Menu.Root

A list of actions and destinations that opens from a trigger; keyboard and focus follow the menu pattern. The popup is raised above the page, with rows of the scale's row height, full-width hairline separators and small keys for shortcuts.

PropTypeDescription
triggerrequired
ReactElement
The control that opens the menu, such as a Button or an AvatarButton; it receives the menu button semantics and ref, so it must accept native props and a ref.
aria-labelDefault the trigger's name, such as Account
string
Accessible name of the menu, where its trigger's name doesn't say it.
childrenrequired
ReactNode
Menu.Header, Menu.Item, Menu.Group, Menu.RadioGroup, Menu.CheckboxItem, Menu.Submenu and Menu.Separator elements.
sideDefault 'bottom'
'top''bottom''left''right''inline-start''inline-end'
The side of the trigger the menu opens on; it flips to the other side where the window has no room. inline-start and inline-end follow the text direction.
alignDefault 'end'
'start''center''end'
How the menu lines up with the trigger along that side.
open
boolean
Whether the menu is open, for a controlled menu; leave it out and the menu opens and closes itself.
defaultOpenDefault false
boolean
Whether an uncontrolled menu starts open.
onOpenChange
(open: boolean, details: OpenChangeDetails) => void
Called with the new visibility, and why, as the menu opens or closes.
className
string
Class of the menu popup. ref, style, data attributes and the other native props go to the popup too; the trigger is the element you pass.
skeletonDefault false
boolean
Shows it and its content as a busy skeleton while that loads; turning it on or off renders the content anew.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Menu.Header

Non-interactive context at the top of a menu, such as the signed-in account.

PropTypeDescription
childrenrequired
ReactNode
The context, such as the signed-in person's avatar, name and e-mail address.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Menu.Item

One action (onClick) or destination (href) in a Menu. Destinations use the LinkProvider's link and close the menu as they are followed.

PropTypeDescription
toneDefault 'neutral'
'neutral''danger'
The item's tone: danger marks a destructive action, such as Delete, its label and icon in the danger color; its row highlights as any other's.
disabledDefault false
boolean
The item is unavailable: it stays in place, dimmed, and reports aria-disabled; its action does not run, and a destination has no link to follow.
aria-current
AriaAttributes['aria-current']
Marks a destination as the page shown now, such as aria-current="page" on the current one of a NavigationBar's destinations that its More menu holds.
aria-label
string
The item's accessible name, where its label and end content don't say it as wanted, such as Inbox, 3 new for a destination whose count shows as a badge.
className
string
Class of the item's row.
icon
ReactNode
A decorative icon before the label, such as Settings.
childrenrequired
ReactNode
The item's label, which names it.
description
ReactNode
A second, muted line; announced as the item's description.
shortcut
string
Keyboard hint at the end, such as Ctrl+P, shown as Kbd keys and announced as aria-keyshortcuts; a sequence, such as Ctrl+K Ctrl+S, shows its keys without the attribute. The application handles the shortcut itself.
end
ReactNode
End content, such as a Badge or a count; it is part of the item's name.
href
string
The destination: the item is a link, through the LinkProvider's link component, and the menu closes as it is followed.
onClick
(event: MouseEvent<HTMLElement>) => void
Runs as the destination is followed, before the menu closes, such as to record the choice; the link still navigates.
target
string
Where the destination opens, such as _blank, as on a native link.
rel
string
The destination's relationship to the page, such as noopener, as on a native link.

Also accepts the native props of an HTML element, such as ref, className and aria-* attributes.

Menu.Separator

Separates groups of menu items.

No props of its own.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Menu.Submenu

A row that opens a nested menu beside it, on hover or with the keyboard. The submenu opens in the theme of its menu, and lines its first row up with the row that opened it.

PropTypeDescription
labelrequired
ReactNode
The label of the row that opens the submenu, which names the row.
childrenrequired
ReactNode
The submenu's items: Menu.Item, Menu.Group, Menu.Separator and the other parts, nested submenus too.
aria-label
string
Accessible name of the submenu; by default the label, when it is a string.
disabledDefault false
boolean
The row stays in place, dimmed, but cannot open its submenu.
open
boolean
Controlled visibility; onOpenChange also suits loading the items as the submenu opens.
defaultOpenDefault false
boolean
Whether an uncontrolled submenu starts open.
onOpenChange
(open: boolean, details: OpenChangeDetails) => void
Called with the new visibility, and why, as the submenu opens or closes, for example to load its items.
className
string
Class of the submenu popup.
ref
Ref<HTMLDivElement>
The submenu popup's ref.
style
CSSProperties
The submenu popup's inline style.
icon
ReactNode
A decorative icon before the label, such as Settings.
description
ReactNode
A second, muted line; announced as the item's description.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Menu.Group

A labelled group of related items, with a small heading above its rows.

PropTypeDescription
labelrequired
ReactNode
Small heading that names the group for assistive technology too.
childrenrequired
ReactNode
Items of the group.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Menu.RadioGroup

One choice among several items; the checked item shows a check mark.

PropTypeDescription
value
stringnull
Value of the checked item, or null for none, for a group the application controls.
defaultValueDefault null
stringnull
Value of the item an uncontrolled group starts with checked, or null for none.
onValueChange
(value: string) => void
Called with the chosen item's value.
childrenrequired
ReactNode
Menu.RadioItem elements.
label
ReactNode
Optional heading; the group is then labelled like Menu.Group.
aria-labelledby
string
Id of a heading elsewhere in the menu that names the group, in place of label.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Menu.RadioItem

One option of a Menu.RadioGroup.

PropTypeDescription
valuerequired
string
The value the Menu.RadioGroup reports when this item is chosen.
disabledDefault false
boolean
The item stays in place, dimmed, but cannot be chosen.
closeOnClickDefault true
boolean
Close the menu after choosing.
className
string
Class of the item's row.
childrenrequired
ReactNode
The item's label, which names it.
end
ReactNode
End content, such as a Badge or a count; it is part of the item's name.
icon
ReactNode
A decorative icon before the label, such as Settings.
description
ReactNode
A second, muted line; announced as the item's description.

Menu.CheckboxItem

An independent on/off option; a check mark shows while it is on.

PropTypeDescription
checked
boolean
Whether the option is on, for an item the application controls; a check mark shows while it is.
defaultCheckedDefault false
boolean
Whether an uncontrolled item starts on.
onCheckedChange
(checked: boolean) => void
Called with the new state when the item is toggled.
disabledDefault false
boolean
The item stays in place, dimmed, but cannot be toggled.
closeOnClickDefault false
boolean
Close the menu after toggling; off by default, so several options can change.
className
string
Class of the item's row.
icon
ReactNode
A decorative icon before the label, such as Settings.
childrenrequired
ReactNode
The item's label, which names it.
description
ReactNode
A second, muted line; announced as the item's description.
shortcut
string
Keyboard hint at the end, such as Ctrl+P, shown as Kbd keys and announced as aria-keyshortcuts; a sequence, such as Ctrl+K Ctrl+S, shows its keys without the attribute. The application handles the shortcut itself.
end
ReactNode
End content, such as a Badge or a count; it is part of the item's name.

ContextMenu

Actions for an area, opened by right click, long press, the ContextMenu key or Shift+F10. In a contained PortalContainer, such as a preview of an interface, it is part of the container as its other overlays are: it opens by right click and the keyboard without hiding the page, locking its scroll or taking focus.

PropTypeDescription
aria-labelrequired
string
Accessible name of the menu, such as Document actions.
contentrequired
ReactNode((target: Element | null) => ReactNode)
Menu content: Menu.Item, Menu.Group, Menu.Separator and the other menu parts. A function receives the element the menu was opened on: the deepest element under the pointer, or the focused element (its active descendant, when it has one) for the keyboard. Where it returns null, undefined, false or '', the menu doesn't open on that element and the browser keeps its own.
childrenrequired
ReactElement
One element that becomes the region; it must accept native props and a ref.
disabledDefault false
boolean
The region keeps the browser's own context menu.
open
boolean
Controlled visibility, for example to open the menu from another control.
onOpenChange
(open: boolean, details: ContextMenuOpenChangeDetails) => void
Called with the new visibility, and why, as the menu opens or closes.
anchorPoint
ContextMenuPoint
Viewport point to open at while controlled; otherwise the menu opens at the pointer.
className
string
Class of the menu popup.
ref
Ref<HTMLDivElement>
The menu popup's ref; the region is the element you pass as children.
style
CSSProperties
The menu popup's inline style.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

ThemeMenuRadioGroup

The theme choice for a Menu: System, Light and Dark as radio items. Inside a ThemePreferenceProvider it shows and stores the provider's preference; value and onValueChange connect it to a theme the application stores itself.

PropTypeDescription
valueDefault the nearest ThemePreferenceProvider's preference
'light''dark''system'
The chosen preference: system, light or dark.
onValueChangeDefault the nearest ThemePreferenceProvider's setPreference, which stores the choice
(value: ThemePreference) => void
Called with the preference chosen, for an application that stores the theme itself.
label
ReactNode
A small heading above the choices, which names the group. Leave it out inside a submenu, which its row already names.
aria-labelledby
string
Id of a heading elsewhere in the menu that names the group, in place of label.
closeOnClickDefault false
boolean
Close the menu after a choice. Off by default, so people see the theme change with the menu still open.

Part names

Each part's class and state attributes are public and versioned, for an application's CSS to style.

env-menu
The menu's popup.
env-menu-choice
A radio or checkbox item.
env-menu-group
A group of items.
env-menu-group-label
A group's label.
env-menu-header
The header at the top of the menu.
env-menu-item
One item.
env-menu-item-description
An item's description.
env-menu-item-end
The end of an item, for a shortcut or a chevron.
env-menu-item-indicator
The check of a chosen item.
env-menu-item-indicator-slot
The place for a chosen item's check.
env-menu-item-label
An item's label.
env-menu-item-shortcut
An item's keyboard shortcut.
env-menu-item-text
The column with an item's label and description.
env-menu-positioner
The menu's positioner.
env-menu-separator
The line between groups.
env-menu-submenu-chevron
The chevron of an item that opens a submenu.
env-menu-submenu-trigger
An item that opens a submenu.
data-checked
Any value, or present on env-menu-choice
data-disabled
Any value, or present on env-menu-item
data-highlighted
Any value, or present on env-menu-choice, env-menu-item
data-instant
group
data-popup-open
Any value, or present on env-menu-choice, env-menu-item
data-tone
danger · neutral on env-menu-item

Color properties

Each sets a color of one instance, through style or CSS; its hover, focus and press follow it. They are public and versioned.

--env-menu-item-container
The item's fill; the highlight and press are state layers over it.
--env-menu-item-content
The label, and the color of the state layers.
--env-menu-item-icon
The leading icon.

Strings

EnvUIProvider's messages overrides each, and a component's own label prop wins over it.

messages.themePreference.toDark
"Switch to dark theme" ThemeToggle's name in the light theme.
messages.themePreference.toLight
"Switch to light theme" ThemeToggle's name in the dark theme.
messages.themePreference.system
"System" The choice that follows the system's theme, as in a theme menu.
messages.themePreference.light
"Light" The light theme's choice.
messages.themePreference.dark
"Dark" The dark theme's choice.

Changelog

1.0.0

ThemeMenuRadioGroup, the theme choice for any menu. The parts are members of the Menu namespace: Menu is Menu.Root, and MenuItem, MenuHeader, MenuSeparator, MenuGroup, MenuRadioGroup, MenuRadioItem, MenuCheckboxItem and MenuSubmenu are Menu.Item, Menu.Header, Menu.Separator, Menu.Group, Menu.RadioGroup, Menu.RadioItem, Menu.CheckboxItem and Menu.Submenu. Menus have one anatomy at the scale's row height: density and the MenuDensity type are gone. OrganizationMenu, OrganizationSwitcher in 1.0.0-prerelease.0, is the organization switcher pattern, built from Menu.Root, Menu.RadioGroup and Menu.RadioItem. ContextMenu keeps its name, and a content function that returns null leaves the menu closed on that element. Menu.Item's trailing is end. A Menu.Item with href closes the menu as it is followed and takes an onClick, which runs as it is followed, and a disabled one renders no link. Every menu's popup is the shared popup surface, which takes the direction at its trigger, and Menu.Root takes side="inline-start" and "inline-end" and passes ref, style and native props to its popup. A chosen item's check is env-menu-item-indicator, as in Select and Combobox, in its place, env-menu-item-indicator-slot; they were env-menu-item-check-mark and env-menu-item-check.

1.0.0-prerelease.6

Colors under the pointer and while pressed change with --env-motion-ease-enter, rather than easing in slowly, so hover and press show from the first frame.

A menu opens over --env-motion-duration-medium, fading in and growing from 98 % with --env-motion-ease-enter, rather than growing from 96 % over the slow duration, so it shows from its first frames rather than late.

A shortcut is muted text in one unit, such as Ctrl+D, rather than a filled chip for each key, so a menu of shortcuts stays quiet down its rows.

A separator is a break of half a row, rather than a gap that read as uneven spacing between rows, and so is the space before a group without a heading; it still draws no line.

In an item with a description, the check mark, the icon, the end and the shortcut sit on the label's line rather than across both lines.

A group with a heading sits closer to the rows before it, and its heading closer to its rows, so a menu of a few groups stays compact; a separator, such as the one before a destructive item, keeps the full space.

A disabled item's label takes the disabled text color, --env-color-fg-disabled, rather than the muted color its description has, so unavailable and explanatory text no longer look alike.

1.0.0-prerelease.5

Menu.Root's aria-label is optional: the menu takes its trigger's name. Menu.RadioGroup takes defaultValue and Menu.CheckboxItem defaultChecked, for choices the menu keeps; Menu.Separator, Menu.Group and Menu.RadioGroup pass their native props on. onOpenChange of Menu.Root, Menu.Submenu and ContextMenu receives why.

ThemeRadioGroup is ThemeMenuRadioGroup, with ThemeMenuRadioGroupProps: it works only inside a menu.

Menus are frosted: the popup's color lets a little of what is behind through, blurred, with a soft shadow and no outline edge but a faint hairline in dark, and is solid with its edge at high contrast and with reduced transparency. Rows are the md control height in body text, in a menu with 1.25 rem corners; separators and groups part by space, with no hairline, and shortcut keys are muted chips with no edge, side by side with the + read but not shown. The highlighted row, under the pointer or the keyboard, takes the neutral fill with no focus ring, and a chosen radio item takes the selection's fill and text as well as its check mark. Group labels sit 10 px from the menu's edge, a little ahead of the rows' icons and text, rather than in line with the labels.