Menus
Menus display a list of choices on a temporary surface. The MD3 Expressive Menu supports shape morphing, selection states, nested submenus, and vibrant color variants.
Menus appear upon interaction with a button, action, or other control. They display a list of choices, one per line. The MD3 Expressive system introduces sophisticated motion, shape morphing, and high-fidelity color tokens to traditional M3 menus.
Introduction
The MD3 Expressive Menu is a high-density navigation and action component. It supports two design variants: Baseline (standard MD3) and Expressive (dynamic morphing). Built on top of Radix UI's Menu primitives, it ensures perfect positioning, keyboard navigation, and accessibility while providing the premium "Expressive" look and feel unique to this library.
Anatomy
- Trigger: The element (e.g., Button, Icon) that opens the menu.
- Content Panel: The floating surface holding the menu items.
- Menu Item: An individual choice. Can include leading icons, trailing text (shortcuts), and supporting text.
- Submenu: A nested menu that opens from a parent menu item.
- Divider / Gap: Visual separators between groups of related items.
- Active Indicator: Marks a selected item, often with a checkmark and shape morphing.
Variants
Design Variants
- Baseline: Standard Material Design 3 look. Clean, predictable, and familiar.
- Expressive: Features fluid shape morphing and scaling transitions. Items "bloom" into view with spring physics.
Color Variants
- Standard: Uses the Surface Container roles.
- Vibrant: Uses Tertiary Container roles for high emphasis. Exclusive to Vertical menus.
Features
Selection States
Menus support single or multiple selection states. In the Expressive variant, selected items feature enhanced visual feedback and shape morphing.
Submenus
For complex hierarchical actions, use nested Submenus. The system handles positioning and focus management automatically.
Icons & Shortcuts
Reinforce meaning with leadingIcon and provide efficiency with trailingText (shortcuts like "⌘C").
Select & Autocomplete (Exposed Dropdown)
The <Select /> component implements the Material Design 3 Expressive Exposed Dropdown Menu spec. It combines a TextField (Outlined or Filled) with a Menu popover, supporting both standard selection and real-time Autocomplete filtering (searchable={true}).
Standard Select
A clean single-select dropdown with floating label and animated trailing arrow.
Autocomplete Mode
Set searchable={true} to transform the Select into a real-time searchable input field that filters options dynamically as the user types.
Outlined vs Filled Variants
<Select /> supports all TextField styling variants (outlined | filled), supporting text, and option leading icons.
Divider & Separator Customization
Control whether horizontal dividers appear between dropdown options with showDividers={false} or specify separatorStyle="none" | "divider" | "gap".
Async Autocomplete & Loading State
For remote APIs, combine searchable, onSearchChange, and loading to display a built-in progress indicator while fetching results asynchronously.
Large Datasets & Virtualization
Progressive chunk loading via maxVisible and IntersectionObserver enables effortless scrolling and searching across 1,000+ items without UI stutter.
Usage
Basic Usage
import {
Menu,
MenuTrigger,
MenuContent,
MenuItem,
Button,
Select
} from "@bug-on/m3-expressive";
export function Example() {
return (
<Menu>
<MenuTrigger asChild>
<Button>Open Menu</Button>
</MenuTrigger>
<MenuContent>
<MenuItem leadingIcon={<Icon name="edit" />}>Edit</MenuItem>
<MenuItem leadingIcon={<Icon name="share" />}>Share</MenuItem>
<MenuItem leadingIcon={<Icon name="delete" />} disabled>Delete</MenuItem>
</MenuContent>
</Menu>
);
}
Select Usage
import { Select } from "@bug-on/m3-expressive";
export function SelectExample() {
const [state, setState] = React.useState("ca");
return (
<Select
label="State / Province"
options={[
{ label: "California", value: "ca" },
{ label: "New York", value: "ny" }
]}
value={state}
onChange={(val) => setState(val)}
showDividers={false}
/>
);
}
Autocomplete Usage (Local Search)
<Select
searchable
label="Search employees"
placeholder="Type a name..."
options={employeesList}
onChange={(val, option) => setSelected(option)}
emptyText="No employees found"
/>
Async Autocomplete Usage (Remote / Server-side)
<Select
searchable
loading={isLoading}
label="Search Countries"
placeholder="Type country name..."
options={searchResults}
onSearchChange={(query) => fetchCountriesFromAPI(query)}
onChange={(val, option) => setSelectedCountry(option)}
/>
Best Practices
Do
- Use menus for a list of related actions that are hidden by default to save space.
- Use icons to help users scan the list more quickly.
- Group related items using dividers or gaps.
- Ensure shortcuts (trailing text) follow system-standard conventions.
- Use
<Select searchable />when the list contains more than 7-10 options to enable fast filtering. - Use
showDividers={false}for shorter, cleaner dropdowns where items do not need explicit separators.
Don't
- Don't use a menu for only one item; use a single button instead.
- Don't create deeply nested submenus (more than 2 levels); it becomes difficult to navigate.
- Avoid putting too much content in a single menu item (keep labels short).
Accessibility
- Keyboard: Full support for Arrow keys,
Home/End,Escape, and character search. - ARIA: Automatically applies
role="menu",role="menuitem", andaria-haspopup. - Focus: Focus is trapped within the menu and returned to the trigger on close.
- Screen Readers: Supports
supportingTextwhich is read alongside the item label.
API Reference
Menu
Root provider.
| Prop | Type | Default | Description |
|---|---|---|---|
menuVariant | "baseline" | "expressive" | "baseline" | Design style. |
colorVariant | "standard" | "vibrant" | "standard" | Color role. |
open | boolean | — | Controlled open state. |
onOpenChange | (open: boolean) => void | — | Change callback. |
MenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
leadingIcon | ReactNode | — | Icon at the start. |
trailingText | string | — | Shortcut (e.g., "⌘C"). |
selected | boolean | false | Shows active state. |
supportingText | ReactNode | — | Helper text below label. |
disabled | boolean | false | Blocks interaction. |
onClick | () => void | — | Click handler. |
Select
| Prop | Type | Default | Description |
|---|---|---|---|
options | SelectOption[] | Required | Array of selectable options. |
value | string | — | Controlled selected value. |
defaultValue | string | — | Uncontrolled initial value. |
onChange | (value: string, option?: SelectOption) => void | — | Callback when selection changes. |
onSearchChange | (query: string) => void | — | Callback for remote/async search. Bypasses internal client filtering. |
searchable | boolean | false | Enables Autocomplete mode with real-time text filtering. |
loading | boolean | false | Displays a loading spinner inside the dropdown during async fetch. |
showDividers | boolean | true | Whether to show horizontal divider lines between options. |
separatorStyle | "divider" | "gap" | "none" | "divider" | Fine-grained separator appearance between menu options. |
maxVisible | number | 50 | Maximum items rendered per chunk in virtual scroll window. |
variant | "outlined" | "filled" | "outlined" | TextField border style. |
label | string | — | Floating input label text. |
placeholder | string | — | Input placeholder. |
supportingText | string | — | Helper text shown below the input. |
colorVariant | "standard" | "vibrant" | "standard" | Dropdown menu color theme. |
menuVariant | "expressive" | "baseline" | "expressive" | Visual design style of dropdown menu. |
matchTriggerWidth | boolean | true | When true, popover width automatically matches input width. |
emptyText | string | "No options found" | Message displayed when searchable filter yields no results. |
renderOption | (option, state) => ReactNode | — | Custom renderer for individual option items. |
renderContent | (state) => ReactNode | — | Custom renderer for complete dropdown menu content. |
disabled | boolean | false | Disables the select input and menu. |
SelectOption
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Display text for the option. |
value | string | Yes | Unique string identifier. |
disabled | boolean | No | Disables selection for this option. |
leadingIcon | ReactNode | No | Icon rendered at start of menu item. |
supportingText | ReactNode | No | Secondary helper text below option label. |