Menu
A dropdown menu anchored to a trigger, with full keyboard navigation.
import { ... } from '@corndev/ui-overlays' HTML
<div class="corn-menu" role="menu">
<div class="corn-menu-group" role="group" aria-labelledby="share-label">
<div class="corn-menu-label" id="share-label">Share</div>
<button class="corn-menu-item" role="menuitem">Copy link</button>
</div>
<button class="corn-menu-item" role="menuitem">Edit</button>
<div class="corn-menu-separator" role="separator"></div>
<button class="corn-menu-item" role="menuitem" data-danger>Delete</button>
</div>
Classes
| Class |
|---|
.corn-menu |
.corn-popover |
.corn-menu-item |
.corn-menu-group |
.corn-menu-label |
.corn-menu-separator |
Attributes
| On | Attribute | Values |
|---|---|---|
.corn-menu | data-placement | top |
.corn-menu | data-visible | present or absent |
.corn-menu-item | data-danger | present or absent |
Vanilla JS
Required for behaviour — adds positions against the trigger and handles arrow/Home/End/Escape navigation.
import { createMenu } from '@corndev/ui-overlays';
import '@corndev/ui/theme';
const triggerEl = document.querySelector('#target');
const instance = createMenu(triggerEl, { /* options */ });
Options
| Option | Type | Default | Description |
|---|---|---|---|
items | Array<MenuItemSpec|MenuGroupSpec> | required | Menu entries. An entry with group and items is a labelled group — the same shape as options elsewhere. |
placement | 'bottom'|'top' | 'bottom' | Preferred placement (flips if no room). |
closeOnSelect | boolean | true | Close the menu after selecting an item. |
onSelect | (value: *, item: MenuItemSpec) => void | undefined | Called when any item is selected. |
React
Menu, MenuItem, MenuSeparator, MenuGroup — from @corndev/ui-overlays-react.
There is no items prop — pass the trigger element as trigger and compose MenuItem / MenuSeparator children instead.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | React.ReactElement | required | Element that opens the menu (cloned with aria + handlers). |
placement | 'bottom'|'top' | 'bottom' | Preferred placement (flips if no room). |
closeOnSelect | boolean | true | Close after an item is selected. |
onSelect | (value: *) => void | undefined | Called with the selected item’s value. |
className | string | undefined | |
children | React.ReactNode | undefined | MenuItem / MenuSeparator / MenuGroup nodes. |
<MenuItem> props
A selectable entry. Fires the menu onSelect, plus its own if given. danger for a destructive one.
| Prop | Type | Default | Description |
|---|---|---|---|
value | * | undefined | Value passed to the menu’s onSelect. |
disabled | boolean | undefined | |
danger | boolean | false | A destructive action — “Delete”, “Revoke”. Coloured as danger; the label is what says so, so keep it a verb that does. |
onSelect | (value: *) => void | undefined | Per-item select handler. |
className | string | undefined | |
children | React.ReactNode | undefined |
<MenuGroup> props
Items under a non-interactive heading that names the group.
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | required | The heading. |
className | string | undefined | |
children | React.ReactNode | undefined | MenuItem nodes. |
Menu is a dropdown anchored to a trigger button. It manages focus and
keyboard navigation (↑/↓ to move, Home/End, Enter/Space to select, Esc to
close and return focus to the trigger) and exposes proper menu/menuitem
roles and aria-haspopup/aria-expanded.
React
import { Menu, MenuItem, MenuSeparator } from '@corndev/ui-overlays-react';
import { Button } from '@corndev/ui-react';
<Menu trigger={<Button>Actions ▾</Button>} onSelect={(value) => console.log(value)}>
<MenuItem value="edit">Edit</MenuItem>
<MenuItem value="duplicate">Duplicate</MenuItem>
<MenuSeparator />
<MenuItem value="delete" disabled>Delete</MenuItem>
</Menu>
Vanilla JS
Pass items as data; the menu builds the markup and wires keyboard nav.
import { createMenu } from '@corndev/ui-overlays';
import '@corndev/ui-overlays/styles';
const menu = createMenu(document.querySelector('#trigger'), {
items: [
{ label: 'Edit', value: 'edit' },
{ label: 'Duplicate', value: 'duplicate' },
{ separator: true },
{ label: 'Delete', value: 'delete', disabled: true },
],
onSelect: (value) => console.log(value),
});
Groups and destructive items
<Menu trigger={<Button>Actions</Button>} onSelect={run}>
<MenuItem value="edit">Edit</MenuItem>
<MenuGroup label="Share">
<MenuItem value="link">Copy link</MenuItem>
<MenuItem value="email">Email</MenuItem>
</MenuGroup>
<MenuSeparator />
<MenuItem value="delete" danger>Delete</MenuItem>
</Menu>
MenuGroup puts items under a heading. The heading is not an item: the arrow
keys pass over it, and it names the group, so a screen reader announces “Share,
group” as focus enters. In the vanilla items array a group is { group: 'Share', items: [...] } — the same shape as options elsewhere.
danger colours a destructive item. The colour backs up the label rather than
replacing it: “Delete” says what it does to anyone who cannot see red, so keep a
danger item’s label a verb that does.