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

OnAttributeValues
.corn-menudata-placementtop
.corn-menudata-visiblepresent or absent
.corn-menu-itemdata-dangerpresent 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

OptionTypeDefaultDescription
itemsArray<MenuItemSpec|MenuGroupSpec>requiredMenu 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).
closeOnSelectbooleantrueClose the menu after selecting an item.
onSelect(value: *, item: MenuItemSpec) => voidundefinedCalled 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

PropTypeDefaultDescription
triggerReact.ReactElementrequiredElement that opens the menu (cloned with aria + handlers).
placement'bottom'|'top''bottom'Preferred placement (flips if no room).
closeOnSelectbooleantrueClose after an item is selected.
onSelect(value: *) => voidundefinedCalled with the selected item’s value.
classNamestringundefined
childrenReact.ReactNodeundefinedMenuItem / MenuSeparator / MenuGroup nodes.

A selectable entry. Fires the menu onSelect, plus its own if given. danger for a destructive one.

PropTypeDefaultDescription
value*undefinedValue passed to the menu’s onSelect.
disabledbooleanundefined
dangerbooleanfalseA destructive action — “Delete”, “Revoke”. Coloured as danger; the label is what says so, so keep it a verb that does.
onSelect(value: *) => voidundefinedPer-item select handler.
classNamestringundefined
childrenReact.ReactNodeundefined

Items under a non-interactive heading that names the group.

PropTypeDefaultDescription
labelReact.ReactNoderequiredThe heading.
classNamestringundefined
childrenReact.ReactNodeundefinedMenuItem 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.