Menu
A toggleable contextual overlay for links, buttons, and forms (formerly Dropdown), with dynamic placement, keyboard navigation, and submenu support.
Overview
Menu is a toggleable contextual overlay for links, buttons, and inline forms. It replaces the old CxDropdown family, is positioned using React Aria's useOverlayPosition, and handles keyboard navigation and multi-level submenus. Menus open on click by default.
Examples
Compose Menu around a MenuToggle and a MenuList. MenuToggle renders the trigger button and MenuList renders the panel — Chassis positions it relative to the toggle automatically.
import { Menu, MenuToggle, MenuList, MenuItem, MenuDivider } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Toggle menu</MenuToggle>
<MenuList>
<MenuItem href="#">Action</MenuItem>
<MenuItem href="#">Another action</MenuItem>
<MenuDivider />
<MenuItem href="#">Something else here</MenuItem>
</MenuList>
</Menu>
)
}Menu items
Both links and buttons work as MenuItem elements. Add active to mark the current selection, or disabled to prevent interaction.
import { Menu, MenuToggle, MenuList, MenuItem } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Toggle menu</MenuToggle>
<MenuList>
<MenuItem href="#">Regular link</MenuItem>
<MenuItem href="#" active>
Active link
</MenuItem>
<MenuItem href="#" disabled>
Disabled link
</MenuItem>
</MenuList>
</Menu>
)
}Selected
Add selected to a MenuItem to indicate the current choice in a selection list. Pair it with a checkmark icon styled with the menu-item-check class, which is only shown on the selected item.
import { Menu, MenuToggle, MenuList, MenuItem } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Sort by</MenuToggle>
<MenuList>
<MenuItem component="button" selected>
Name
</MenuItem>
<MenuItem component="button">Date modified</MenuItem>
<MenuItem component="button">Size</MenuItem>
</MenuList>
</Menu>
)
}Rich content
Add icon/description to a MenuItem for a leading icon and a secondary line of text below the label.
import { Icon, Menu, MenuToggle, MenuList, MenuItem } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Switch workspace</MenuToggle>
<MenuList>
<MenuItem
component="button"
icon={<Icon name="shield-outline" size={16} />}
description="3 members"
selected
>
Acme Corp
</MenuItem>
<MenuItem
component="button"
icon={<Icon name="users-outline" size={16} />}
description="12 members"
>
Globex Inc
</MenuItem>
</MenuList>
</Menu>
)
}Toggle without a caret
MenuToggle draws a caret after its content. Set caret={false} for a toggle that shows what it is another way, such as an icon-only button, and give it an aria-label.
<MenuToggle caret={false} aria-label="More actions">
<Icon name="ellipsis-h-solid" />
</MenuToggle>Menu content
Headers
Use MenuHeader to label a group of items.
import { Menu, MenuToggle, MenuList, MenuItem, MenuHeader } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Toggle menu</MenuToggle>
<MenuList>
<MenuItem href="#">Copy</MenuItem>
<MenuItem href="#">Paste</MenuItem>
<MenuHeader>Danger zone</MenuHeader>
<MenuItem href="#">Delete all</MenuItem>
</MenuList>
</Menu>
)
}Dividers
Place MenuDivider between groups of related items.
import { Menu, MenuToggle, MenuList, MenuItem, MenuDivider } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Toggle menu</MenuToggle>
<MenuList>
<MenuItem href="#">Copy</MenuItem>
<MenuItem href="#">Cut</MenuItem>
<MenuDivider />
<MenuItem href="#">Paste</MenuItem>
</MenuList>
</Menu>
)
}Text
MenuText applies header padding to non-interactive content, letting it sit flush with adjacent items.
import { Menu, MenuToggle, MenuList, MenuItem, MenuText } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Toggle menu</MenuToggle>
<MenuList>
<MenuText>Clipboard</MenuText>
<MenuItem href="#">Copy</MenuItem>
<MenuItem href="#">Cut</MenuItem>
<MenuItem href="#">Paste</MenuItem>
</MenuList>
</Menu>
)
}Forms
A menu list can contain any form content.
import {
Button,
Form,
Checkbox,
TextInput,
FormLabel,
Menu,
MenuToggle,
MenuList
} from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Toggle menu</MenuToggle>
<MenuList style={{ '--cx-menu-min-width': '300px' } as React.CSSProperties}>
<Form className="vstack gap-md p-md">
<div>
<FormLabel htmlFor="menuFormEmail" id="menuFormEmailLabel">
Email address
</FormLabel>
<TextInput
type="email"
id="menuFormEmail"
aria-labelledby="menuFormEmailLabel"
placeholder="email@example.com"
/>
</div>
<div>
<FormLabel htmlFor="menuFormPassword" id="menuFormPasswordLabel">
Password
</FormLabel>
<TextInput
autoComplete="current-password"
type="password"
id="menuFormPassword"
aria-labelledby="menuFormPasswordLabel"
placeholder="Password"
/>
</div>
<Checkbox id="menuRemember" label="Remember me" />
<Button type="submit" color="primary">
Sign in
</Button>
</Form>
</MenuList>
</Menu>
)
}Data-driven items
Pass an items array to MenuList instead of composing children to build a flat list from data — items, headers, and dividers interleaved in authoring order. Covers flat items, headers, and dividers only; compose with children and MenuSubmenu directly for nested submenus.
import { Icon, Menu, MenuToggle, MenuList } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Recent files</MenuToggle>
<MenuList
items={[
{ id: 'new', label: 'New file', href: '#' },
{ id: 'open', label: 'Open…', href: '#' },
{ type: 'divider', id: 'divider' },
{ type: 'header', id: 'recent', label: 'Recent' },
{
id: 'report',
label: 'Quarterly report.docx',
icon: <Icon name="file-circle-check-outline" size={16} />,
href: '#'
},
{
id: 'notes',
label: 'Meeting notes.docx',
icon: <Icon name="file-circle-check-outline" size={16} />,
href: '#'
}
]}
/>
</Menu>
)
}Placement
Pass a placement prop to control which side of the toggle the menu opens on. It flips automatically when viewport space is limited.
import { Menu, MenuToggle, MenuList, MenuItem } from '@chassis-ui/react'
const placements = ['top', 'bottom', 'left', 'right'] as const
export const Example = () => {
return (
<div className="d-flex flex-wrap gap-sm">
{placements.map((placement) => (
<Menu key={placement} placement={placement}>
<MenuToggle color="secondary">{placement}</MenuToggle>
<MenuList>
<MenuItem href="#">Action</MenuItem>
<MenuItem href="#">Another action</MenuItem>
<MenuItem href="#">Something else here</MenuItem>
</MenuList>
</Menu>
))}
</div>
)
}Close behavior
The autoClose prop controls which clicks close the menu. The default (true) closes on any click inside or outside. Use 'inside' or 'outside' to restrict to one side, or false to leave closing to the toggle, the Escape key and visible.
import { Menu, MenuToggle, MenuList, MenuItem } from '@chassis-ui/react'
export const Example = () => {
return (
<div className="d-flex flex-wrap gap-sm">
<Menu autoClose>
<MenuToggle color="secondary">Default</MenuToggle>
<MenuList>
<MenuItem href="#">Copy</MenuItem>
<MenuItem href="#">Paste</MenuItem>
<MenuItem href="#">Delete</MenuItem>
</MenuList>
</Menu>
<Menu autoClose="inside">
<MenuToggle color="secondary">Close inside</MenuToggle>
<MenuList>
<MenuItem href="#">New file</MenuItem>
<MenuItem href="#">Open</MenuItem>
<MenuItem href="#">Save</MenuItem>
</MenuList>
</Menu>
<Menu autoClose="outside">
<MenuToggle color="secondary">Close outside</MenuToggle>
<MenuList>
<MenuItem href="#">Rename</MenuItem>
<MenuItem href="#">Duplicate</MenuItem>
<MenuItem href="#">Move to</MenuItem>
</MenuList>
</Menu>
<Menu autoClose={false}>
<MenuToggle color="secondary">Manual close</MenuToggle>
<MenuList>
<MenuItem href="#">Cut</MenuItem>
<MenuItem href="#">Copy</MenuItem>
<MenuItem href="#">Paste</MenuItem>
</MenuList>
</Menu>
</div>
)
}Controlling the menu
Pass visible with onVisibleChange to hold the open state yourself. The menu then opens and closes only when visible changes: the toggle, the clicks autoClose counts and the Escape key are reported to onVisibleChange with the state they ask for. A state setter fits as it is.
import { useState } from 'react'
import { Button, Menu, MenuItem, MenuList, MenuToggle } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<div className="d-flex align-items-center gap-sm">
<Menu visible={visible} onVisibleChange={setVisible}>
<MenuToggle color="secondary">Actions</MenuToggle>
<MenuList>
<MenuItem href="#">Rename</MenuItem>
<MenuItem href="#">Duplicate</MenuItem>
<MenuItem href="#">Move to</MenuItem>
</MenuList>
</Menu>
<Button variant="outline" onClick={() => setVisible(true)}>
Open from here
</Button>
<span className="form-help">Menu is {visible ? 'open' : 'closed'}.</span>
</div>
)
}defaultVisible opens the menu on first render and leaves it uncontrolled. onShow, onShown, onHide and onHidden report what the menu did; they don't carry the state.
Submenus
Wrap a trigger and nested items in MenuSubmenu to create a flyout. Submenus close sibling submenus automatically when a new one opens, and close along with their ancestor menu. Set activation to 'hover' or 'click' to restrict which interaction opens it on hover-capable devices — the default, 'both', accepts either; touch devices always use tap regardless of this setting. Which side a submenu cascades toward follows the active locale's direction — see Internationalization.
import { Menu, MenuSubmenu, MenuToggle, MenuList, MenuItem, MenuDivider } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Submenus</MenuToggle>
<MenuList>
<MenuSubmenu trigger="File">
<MenuItem href="#">New</MenuItem>
<MenuItem href="#">Open</MenuItem>
<MenuItem href="#">Save</MenuItem>
</MenuSubmenu>
<MenuSubmenu trigger="Edit">
<MenuItem href="#">Cut</MenuItem>
<MenuItem href="#">Copy</MenuItem>
<MenuItem href="#">Paste</MenuItem>
</MenuSubmenu>
<MenuSubmenu trigger="View">
<MenuItem href="#">Zoom in</MenuItem>
<MenuItem href="#">Zoom out</MenuItem>
</MenuSubmenu>
<MenuDivider />
<MenuItem href="#">Preferences</MenuItem>
</MenuList>
</Menu>
)
}Submenus on mobile
Set stacked on a MenuSubmenu to switch to a view-replacement pattern below the sm breakpoint, and add MenuSubmenuBack as the first item to let people navigate back to the parent level.
import {
Menu,
MenuSubmenuBack,
MenuSubmenu,
MenuToggle,
MenuList,
MenuItem
} from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Stacked submenus</MenuToggle>
<MenuList>
<MenuItem href="#">Level 1 action</MenuItem>
<MenuSubmenu trigger="Level 1 submenu" stacked>
<MenuSubmenuBack>Level 1</MenuSubmenuBack>
<MenuItem href="#">Level 2 action</MenuItem>
<MenuItem href="#">Another level 2</MenuItem>
</MenuSubmenu>
<MenuItem href="#">Another level 1</MenuItem>
</MenuList>
</Menu>
)
}Dark mode
Apply data-cx-theme="dark" to a Menu or any ancestor element to opt the menu into the dark theme.
import { Menu, MenuToggle, MenuList, MenuItem } from '@chassis-ui/react'
export const Example = () => {
return (
<div data-cx-theme="dark">
<Menu>
<MenuToggle color="secondary">Toggle menu</MenuToggle>
<MenuList>
<MenuItem href="#" active>
Recent
</MenuItem>
<MenuItem href="#">All files</MenuItem>
<MenuItem href="#">Shared with me</MenuItem>
</MenuList>
</Menu>
</div>
)
}Context variants
Add context classes (e.g. context info, context danger) to a MenuList or an individual MenuItem to remap colors to a semantic palette.
import { Menu, MenuToggle, MenuList, MenuItem } from '@chassis-ui/react'
export const Example = () => {
return (
<Menu>
<MenuToggle color="secondary">Toggle menu</MenuToggle>
<MenuList>
<MenuItem href="#" className="context info">
Copy
</MenuItem>
<MenuItem href="#" className="context success">
Save
</MenuItem>
<MenuItem href="#" className="context danger">
Delete
</MenuItem>
</MenuList>
</Menu>
)
}API
Menu
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
autoClose? | MenuAutoClose | — |
Controls which clicks close the menu. true closes on any click inside or outside.
false leaves closing to the toggle, the Escape key and visible. 'inside' closes only
on click inside the menu. 'outside' closes only on click outside the menu. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
component? | ElementType | — |
Component used for the root node. Either a string to use a HTML element or a component.
Defaults to Fragment — MenuToggle/MenuList render with no wrapping element, so
they land as direct children of whatever contains the Menu. This matters inside
ButtonGroup/InputGroup, whose CSS expects the toggle button and menu panel as direct
children rather than nested inside an intermediate node.
Pass an actual element (e.g. "div", or "li" for a navbar item) to opt back into a
wrapper — needed if you want to apply className/style/other rest props to a container
around the whole menu, for semantic wrapping like a nav <li>, or for reference="parent",
which positions off this wrapper and has nothing to measure against without one. | ||
container? | boolean | Element | — |
Teleports the menu panel to a container element on open. Accepts an element reference, or
true to append to document.body, or to the open dialog the menu is in, so the panel
stays in that dialog's top layer. | ||
defaultVisible? | boolean | — |
Whether the menu is open when it first renders. Use it instead of visible when nothing
outside needs to control the menu. | ||
offset? | [number, number] | — |
Distance between the menu and its reference element, as [skidding, distance] in pixels. | ||
onHidden? | (() => void) | — |
| Callback fired after the menu finishes hiding. | ||
onHide? | (() => void) | — |
| Callback fired when the menu starts to hide. | ||
onShow? | (() => void) | — |
| Callback fired when the menu starts to show. | ||
onShown? | (() => void) | — |
| Callback fired after the menu finishes showing. | ||
onVisibleChange? | ((visible: boolean) => void) | — |
Callback fired when the menu asks to show or hide: the toggle, a click that autoClose
counts, or the Escape or Tab key. Receives the state it asks for. With visible set, the
menu changes only when visible does. | ||
placement? | 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end' | — |
| Initial placement. Chassis will flip it to keep the menu in view. | ||
ref? | any | — |
reference? | "toggle" | "parent" | — |
Reference element used for positioning. 'toggle' uses the trigger. 'parent' uses this
component's own rendered element, useful for split buttons and button groups. | ||
visible? | boolean | — |
Whether the menu is open. Setting it makes the menu controlled: it opens and closes only
when this changes, so pair it with onVisibleChange. Leave it unset, or use
defaultVisible, for a menu that opens and closes itself. | ||
MenuToggle
| Prop | Type | Default |
|---|---|---|
active? | boolean | — |
| Toggle the active state for the component. | ||
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
caret? | boolean | true |
Draws the caret after the toggle's content. Set it to false for a toggle that shows what it
is another way, such as an icon-only "more" button. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
color? | ContextColor | — |
Sets the color of the component to one of Chassis context colors. Only applies to the
default Button root. | ||
component? | ElementType | — |
Component used for the root node — the trigger element the menu opens from. Defaults to
Button; swap for e.g. NavLink to render a nav-item-style trigger (.nav-link.caret)
instead of a .button.caret. Its own props are type-checked at the call site once passed
here. | ||
disabled? | boolean | — |
| Toggle the disabled state for the component. | ||
href? | string | — |
| The href attribute specifies the URL of the page the link goes to. | ||
ref? | any | — |
shape? | Shapes | — |
Select the shape of the component. Only applies to the default Button root. | ||
size? | "sm" | "lg" | — |
Size the component sm or lg. Only applies to the default Button root. | ||
variant? | "link" | "basic" | "outline" | "smooth" | — |
Sets the style variant of the component, as Button's own variant does. Only applies to
the default Button root. | ||
MenuList
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
component? | ElementType | — |
| Component used for the root node. Either a string to use a HTML element or a component. | ||
items? | MenuItemsDef | — |
Array of item/header/divider definitions for data-driven rendering. When provided, children
are ignored. Covers flat items, headers, and dividers only — for nested submenus, compose
with children and MenuSubmenu instead. | ||
ref? | any | — |
MenuItem
| Prop | Type | Default |
|---|---|---|
active? | boolean | — |
| Toggle the active state for the component. | ||
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
className? | string | — |
| A string of all className you want applied to the component. | ||
color? | ContextColor | — |
Sets the link color to one of Chassis context colors, including its interactive
(:hover/:focus/:active/:visited) states. | ||
component? | ElementType | — |
| Component used for the root node. Either a string to use a HTML element or a component. | ||
description? | ReactNode | — |
Secondary line of text rendered below children (.menu-item-description). | ||
disabled? | boolean | — |
| Toggle the disabled state for the component. | ||
href? | string | — |
The href attribute specifies the URL of the page the link goes to. Reaches an <a> or a
component reference (a router link); dropped, with a warning in development, when
component is another element. Declared explicitly here — rather than left to flow through
generically from whatever C is — so consumers that wrap Link (MenuItem, ListItem,
NavLink) can read it with a concrete type regardless of component. | ||
icon? | ReactNode | — |
Icon rendered at the item's leading edge (.menu-item-icon). | ||
iconLink? | boolean | — |
Aligns a leading or trailing icon with the link text using flexbox, with a gap between
them and an offset underline. Icons need to be passed as children alongside the text.
Named iconLink rather than icon to avoid colliding with components (e.g. MenuItem)
that already have their own, differently-typed icon prop for the icon content itself. | ||
onClick? | MouseEventHandler<HTMLElement> | — |
| Fires on click. | ||
ref? | any | — |
reset? | boolean | — |
| Removes the foreground color override, so the link inherits its color from the nearest ancestor instead of the default link color. | ||
selected? | boolean | — |
Marks the item as the current selection in a choice list. Renders in a heavier font weight.
Combine with a manually-composed .menu-item-check icon (in children, or via icon on a
plain, non-selection item) to also show a checkmark — this prop alone doesn't add one, so
existing font-weight-only usage keeps rendering unchanged. | ||
stretched? | boolean | — |
Expands the link's click target to fill its positioned ancestor (the nearest ancestor with
a position other than static). | ||
type? | "button" | "reset" | "submit" | — |
Specifies the type of button. Only applies when component="button". Different browsers may
use different default types for the <button> element, so always specify it explicitly. | ||
MenuDivider
| Prop | Type | Default |
|---|---|---|
className? | string | — |
| A string of all className you want applied to the component. | ||
MenuHeader
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
className? | string | — |
| A string of all className you want applied to the component. | ||
component? | ElementType | — |
| Component used for the root node. Either a string to use a HTML element or a component. | ||
ref? | any | — |
role? | AriaRole | — |
ARIA role applied to the root node. Defaults to 'presentation' — see the render function
for why. | ||
MenuText
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
className? | string | — |
| A string of all className you want applied to the component. | ||
component? | ElementType | — |
| Component used for the root node. Either a string to use a HTML element or a component. | ||
ref? | any | — |
MenuSubmenu
| Prop | Type | Default |
|---|---|---|
activation? | "click" | "hover" | "both" | both |
MenuSubmenu activation mode on hover-capable devices. 'click' activates on click only.
'hover' activates on hover only. 'both' (the default) activates on both. Touch
devices always use tap regardless of this setting. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
disabled? | boolean | — |
| Prevents the submenu from opening. | ||
offset? | [number, number] | [-4, 0] |
Distance between the nested menu and the trigger, as [skidding, distance] in pixels. | ||
placement? | Placement | — |
Placement of the nested menu relative to the trigger. Defaults to cascading in the reading
direction — 'right-start' under LTR locales, 'left-start' under RTL ones (see
useLocale) — matching native OS/browser submenu behavior. An explicit value always wins
over that locale-based default. | ||
stacked? | boolean | — |
Switches to a view-replacement pattern below the sm breakpoint. Pair with a
MenuSubmenuBack as the first item of the nested menu. | ||
submenuDelay? | number | 100 |
| Milliseconds before closing the submenu when the pointer leaves it. | ||
trigger | ReactNode | — |
Content of the .menu-item trigger that opens the submenu. | ||
MenuSubmenuBack
| Prop | Type | Default |
|---|---|---|
className? | string | — |
| A string of all className you want applied to the component. | ||