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 asCtrl+D, shown as muted text in one unit and announced asaria-keyshortcuts; endcontent, 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 witharia-disabled, its icon dimmed and its label in the disabled text color, apart from its description, which stays muted and readable to say why.
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
sideisbottom, the default,top,left,right, orinline-startandinline-end, which follow the text direction, and it flips where the window has no room.alignisstart,centerorend, the default.- Leave the menu uncontrolled, with
defaultOpento start open, or control it withopenandonOpenChange, which receives why, such asescape-keyoritem-press. className,ref,styleand 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.
contentcan be a function that receives the element the menu opened on and returns them; one that returnsnullleaves the menu closed there.disabledkeeps the browser's own context menu.open,onOpenChangeandanchorPointopen 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 takesvalueandonValueChange, ordefaultValuealone, and alabel, oraria-labelledby; a radio item closes the menu unlesscloseOnClick={false}. - Menu.CheckboxItem takes
checkedandonCheckedChange, ordefaultCheckedalone, and keeps the menu open unlesscloseOnClick. - ThemeMenuRadioGroup is the theme choice as radio items, from the nearest ThemePreferenceProvider, or from
valueandonValueChange; it keeps the menu open so people see the theme change. - Menu.Submenu is a row with its
labelthat opens its children beside it;open,defaultOpenandonOpenChangesuit 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
onClickruns as it is followed. targetandrelare 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, withdata-highlighted,data-disabledanddata-tone. Its label and description are inenv-menu-item-text,env-menu-item-labelandenv-menu-item-description; its end,env-menu-item-end, holds the shortcut,env-menu-item-shortcut. - A radio or checkbox item,
env-menu-choice, withdata-checked, and its check inenv-menu-item-indicator-slotandenv-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-labelfor every ContextMenu and every icon-only trigger; a Menu.Root takes its trigger's name, unless its ownaria-labelsays 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-haspopupandaria-expandedon the trigger, andmenuitem,menuitemradioandmenuitemcheckboxrows. - 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.
| Prop | Type | Description |
|---|---|---|
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 | ( | 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.
| Prop | Type | Description |
|---|---|---|
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.
| Prop | Type | Description |
|---|---|---|
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[ | 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 | ( | 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.
| Prop | Type | Description |
|---|---|---|
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 | ( | 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< | 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.
| Prop | Type | Description |
|---|---|---|
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.
| Prop | Type | Description |
|---|---|---|
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 | ( | 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.
| Prop | Type | Description |
|---|---|---|
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.
| Prop | Type | Description |
|---|---|---|
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 | ( | 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.
| Prop | Type | Description |
|---|---|---|
aria-labelrequired | string | Accessible name of the menu, such as Document actions. |
contentrequired | 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 | ( | 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< | 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.
| Prop | Type | Description |
|---|---|---|
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 | ( | 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-instantgroupdata-popup-open- Any value, or present on env-menu-choice, env-menu-item
data-tonedanger·neutralon 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
Menunamespace:MenuisMenu.Root, andMenuItem,MenuHeader,MenuSeparator,MenuGroup,MenuRadioGroup,MenuRadioItem,MenuCheckboxItemandMenuSubmenuareMenu.Item,Menu.Header,Menu.Separator,Menu.Group,Menu.RadioGroup,Menu.RadioItem,Menu.CheckboxItemandMenu.Submenu. Menus have one anatomy at the scale's row height:densityand theMenuDensitytype 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 acontentfunction that returnsnullleaves the menu closed on that element. Menu.Item'strailingisend. A Menu.Item withhrefcloses the menu as it is followed and takes anonClick, 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 takesside="inline-start"and"inline-end"and passesref,styleand native props to its popup. A chosen item's check isenv-menu-item-indicator, as in Select and Combobox, in its place,env-menu-item-indicator-slot; they wereenv-menu-item-check-markandenv-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-labelis optional: the menu takes its trigger's name. Menu.RadioGroup takesdefaultValueand Menu.CheckboxItemdefaultChecked, for choices the menu keeps; Menu.Separator, Menu.Group and Menu.RadioGroup pass their native props on.onOpenChangeof 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.