Menu
Commands, links, checkboxes and submenus in a popover anchored to a trigger.
c2-menu is a trigger plus a popover of commands. The trigger goes in the trigger slot — a c2-button, a c2-icon-button, a plain <button> — or stays anywhere on the page and is named by anchor. The rows are c2-menu-item children: a command, a link (href), a checkbox or a radio (type), or the parent of a nested menu slotted into submenu. <hr> draws a separator between them and <h1>–<h6> a group heading.
Activating a row fires menu-select and closes the menu, unless the menu (or that single row) is marked keep-open, which is what a list of checkbox rows wants. The surface is a native popover on the top layer positioned by c2-overlay, so it flips near the bottom of the viewport, light-dismisses on Escape and outside clicks, and is never clipped by an overflow: hidden parent.
Choose where it opens with placement: top, right, bottom or left, optionally aligned with -start or -end. offset sets the gap from the trigger and cross-offset shifts the menu sideways along the trigger. All offsets are pixel numbers, and floating-ui can still flip the menu when the preferred side has no room.
The menu never writes attributes on its own host or its rows, so server-rendered markup matches the hydrated element. While something is slotted into trigger the host is an inline box and matches c2-menu:state(has-trigger); without one it is display: contents. Earlier releases wrote a has-trigger attribute (replace c2-menu[has-trigger] with c2-menu:state(has-trigger)), a placement="right-start" attribute on submenus (the default is now applied without it), and reserve-indicator on the rows (still accepted as an attribute, no longer written). For a plain <button> trigger the menu does write aria-haspopup and aria-expanded on it, since a foreign element has no other way to carry them; render them in your markup (aria-haspopup="menu" aria-expanded="false") and the first write changes nothing.
Keyboard: Enter, Space or ArrowDown on the trigger opens the menu on its first row (ArrowUp on the last). Inside, the arrows move between enabled rows, Home and End jump to the ends, letters jump to a matching row, ArrowRight opens a submenu and ArrowLeft returns from it, and Escape or Tab closes the menu and hands focus back to the trigger.
For a main action with a caret menu of alternatives in one control (“Send ▾”, a remembered merge method), use c2-split-button: it anchors this menu to the whole control and names the caret.
Import
import '@c2n/components/menu'The examples use icons from the separate @c2n/feather-icons package: npm install @c2n/feather-icons.
c2-menu-item ships in the same package and is registered with it.
Usage
Click any trigger below to open its menu.
Signed in as ada
Panels
Density
The surface is as wide as its rows: --c2-menu--min-width (128px) is only a floor, and a menu that owns its trigger is never narrower than that trigger either — so a wide button opens a wide menu. fit-anchor pins it to exactly the trigger’s width, and setting the floor to 0 lets a short menu shrink to its content.
Handling a selection
menu-select bubbles from the activated row: detail.value is that row’s value, detail.checked its state after the activation, and detail.data the payload you put on the row. One listener on the menu covers every row, submenus included.
const menu = document.querySelector('c2-menu')
menu.addEventListener('menu-select', (event) => {
console.log(event.detail.value) // 'delete'
console.log(event.detail.checked) // false for a command, the new state for a checkbox row
console.log(event.detail.data) // { id: 7 }
})
menu.querySelector('c2-menu-item[value="delete"]').data = { id: 7 }
// Open and close it from code; `show('last')` starts on the last row.
menu.show()
menu.hide()
menu.toggle()A trigger elsewhere on the page is named by anchor, and the menu opens beside it:
<button id="row-7">Actions</button>
<c2-menu anchor="row-7" placement="right-start" offset="12" cross-offset="4">
<c2-menu-item value="edit">Edit</c2-menu-item>
</c2-menu>Theming
--c2-menu--* is the surface: background, a border per side, a radius per corner, shadow, padding, min-width and the max-height it starts scrolling at. --c2-menu__separator--* and --c2-menu__heading--* style the <hr> and group headings. The rows are your own children, so --c2-menu-item--* set on the menu cascades into all of them, with __hover, __focus, __disabled, __destructive states and __description, __shortcut, __icon and __indicator parts.
c2-menu.brand {
--c2-menu--border-top-left-radius: 12px;
--c2-menu--border-top-right-radius: 12px;
--c2-menu--border-bottom-left-radius: 12px;
--c2-menu--border-bottom-right-radius: 12px;
--c2-menu--padding-top: 6px;
--c2-menu--padding-right: 6px;
--c2-menu--padding-bottom: 6px;
--c2-menu--padding-left: 6px;
--c2-menu-item__hover--background: #eef2ff;
--c2-menu-item__hover--color: #3730a3;
--c2-menu-item__icon--color: #6366f1;
}Every variable, with its default, is listed on the API tab, and the Gallery tab has ready-made looks to start from.