Menu
Floating dropdown panel for navigation and action menus. Outside-click and Escape dismissal built in.
Menu is a floating dropdown panel anchored to a trigger element. Use for action menus on row context (Edit / Duplicate / Delete), filter menus, and small navigation popovers.
Use it for
- Per-row action menus on tables and lists
- Overflow / "more options" buttons
- Filter selection menus inside a FilterButton pattern
- Small navigation popovers (5-10 destinations)
For a drop-down option picker tied to form state, use Select or FilterButton — those handle their own state. Menu is for cases where the consumer owns open state and the menu is purely presentational.
Import
import { Menu } from '@brikdesigns/bds';Variants
Basic
The consumer owns isOpen state and provides onClose. Menu auto-closes on outside click and Escape.
import { useState } from 'react';
const [open, setOpen] = useState(false);
<div style={{ position: 'relative' }}>
<Button onClick={() => setOpen(!open)}>Options</Button>
<Menu
isOpen={open}
onClose={() => setOpen(false)}
items={[
{ id: '1', label: 'Edit', onClick: handleEdit },
{ id: '2', label: 'Duplicate', onClick: handleDuplicate },
{ id: '3', label: 'Delete', onClick: handleDelete },
]}
style={{ top: '100%', left: 0 }}
/>
</div>With icons
<Menu
isOpen={open}
onClose={() => setOpen(false)}
items={[
{ id: '1', label: 'Edit', icon: <EditIcon />, onClick: handleEdit },
{ id: '2', label: 'Delete', icon: <TrashIcon />, onClick: handleDelete },
]}
/>Active item highlight
activeId highlights the currently selected item — useful when Menu doubles as a "current view" indicator.
<Menu
isOpen={open}
onClose={() => setOpen(false)}
activeId="grid"
items={[
{ id: 'grid', label: 'Grid view' },
{ id: 'list', label: 'List view' },
{ id: 'board', label: 'Board view' },
]}
/>Disabled item
<Menu
isOpen={open}
onClose={() => setOpen(false)}
items={[
{ id: 'edit', label: 'Edit' },
{ id: 'archive', label: 'Archive (Pro plan)', disabled: true },
]}
/>Positioning
Menu doesn't position itself — the consumer applies CSS to place it relative to the trigger. The standard pattern: parent has position: relative, Menu has position: absolute (default) with top / left (or right) set inline.
<Menu isOpen={open} onClose={onClose} items={items} style={{ top: '100%', right: 0 }} />For more sophisticated positioning (collision detection, viewport-aware flips), use a Popover-based solution like DatePicker's underlying pattern.
When not to use
Don't use Menu where Select fits. If the menu drives form state, use Select — it owns its open/close state and integrates with forms. Menu is for presentational dropdowns where the consumer manages everything.
- Don't use Menu for 1-2 items. Wrap a Button or a Tooltip-revealed action instead.
- Don't use Menu without a trigger. It's anchored to something — a button, an icon, a row's overflow control. A floating menu without an obvious source is confusing.
Accessibility
- Menu carries
role="menu"; each item isrole="menuitem". - The trigger should set
aria-haspopup="menu"andaria-expandedto reflect Menu state. - Keyboard arrows move between items;
Enteractivates;Escapecloses (auto-handled). - Outside click closes — auto-handled.
- Focus trap: focus moves into the menu on open; closing returns focus to the trigger.
API
| Prop | Type | Default |
|---|---|---|
items | MenuItemData[] (required) | — |
isOpen | boolean (required) | — |
onClose | () => void (required) | — |
activeId | string | — |
Plus standard <div> HTML attributes (excluding children).
MenuItemData
interface MenuItemData {
id: string;
label: ReactNode;
icon?: ReactNode;
disabled?: boolean;
onClick?: () => void;
}Related
- Select — form-state dropdown
- FilterButton — filter dropdown with its own state machine
- Tooltip — hover-revealed contextual help
- Storybook playground