Menu
A hand-drawn dropdown menu of actions triggered by a button.
Overview
A Menu (dropdown) presents a list of actions triggered by a button — Edit, Duplicate, Delete.
For choosing a persistent value from a list, use Select
instead.
Anatomy
A <button> trigger (aria-haspopup="menu") opens a floating role="menu" of role="menuitem"s.
Both the trigger and the panel are sketchy boxes (@ghds/sketch-core). Data-driven — pass an
items array of { value, label, disabled? }.
Variants & States
| State | Status | Notes |
|---|---|---|
| Closed | Implemented | Default; aria-expanded=false. |
| Open | Implemented | Panel shown; focus moves into the items. |
| Highlighted item | Implemented | The focused menuitem is highlighted. |
| Disabled item | Implemented | Per-item; skipped by keyboard. |
Usage
Do
- Use it for actions on an object, not for form value selection (use Select).
- Keep labels action-oriented (verbs).
- Group destructive actions carefully and consider a confirmation for irreversible ones.
Don't
- Don't overload one menu with many unrelated actions.
- Don't use a Menu where a couple of visible buttons would be clearer.
Accessibility
- React / Web Components:
aria-haspopup="menu"+aria-expandedtrigger;role="menu"panel ofrole="menuitem"s. Enter/Space/ArrowDown open and focus the first item; Arrow/Home/End move focus; Enter/Space activate; Escape closes and restores focus to the trigger; click-outside closes. Web Components dispatches aselectevent. - React Native: a
Modal-anchored panel; selection by tap.accessibilityRole="menu"/"menuitem". Arrow-key roving is web-only. - See the Accessibility Guide.
| Key | Action |
|---|---|
Enter / Space / ↓ | Open the menu and focus the first item. |
Arrow Up / Down | Move focus between items (wraps). |
Home / End | Focus the first / last item. |
Enter / Space | Activate the focused item. |
Escape | Close and return focus to the trigger. |
Content
Action labels (verbs). Keep them short.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Trigger button label. |
items | MenuItem[] | — | { value, label, disabled? } per entry. |
onSelect | (value: string) => void | — | Fires with the activated item value. |
disabled | boolean | false | Disables the whole menu. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
items | GhMenuItem[] | — | { value, label, disabled? } (property). |
label | string | — | Trigger label. |
disabled | boolean | false | Disables the whole menu. |
select event | CustomEvent<string> | — | Dispatched with the activated value. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Trigger button label. |
items | MenuItem[] | — | { value, label, disabled? } per entry. |
onSelect | (value: string) => void | — | Fires with the activated item value. |
disabled | boolean | false | Disables the whole menu. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react
import { Menu } from '@ghds/react/menu';
<Menu
label="Actions"
items={[
{ value: 'edit', label: 'Edit' },
{ value: 'duplicate', label: 'Duplicate' },
{ value: 'archive', label: 'Archive', disabled: true },
{ value: 'delete', label: 'Delete' },
]}
onSelect={(value) => run(value)}
/>;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/menu';
const menu = document.getElementById('actions');
// items is a property, not an attribute
menu.items = [
{ value: 'edit', label: 'Edit' },
{ value: 'delete', label: 'Delete' },
];
menu.addEventListener('select', (e) => run(e.detail));
</script>
<gh-menu id="actions" label="Actions"></gh-menu>
// React Native — @ghds/react-native
import { Menu } from '@ghds/react-native/menu';
<Menu
label="Actions"
items={[
{ value: 'edit', label: 'Edit' },
{ value: 'delete', label: 'Delete' },
]}
onSelect={(value) => run(value)}
/>;