Skip to content
kitn AI/UI

Menu

kai-menu

A cascading action menu you describe with data, not markup. Hand it a tree of items — actions, headings, separators, checkboxes, submenus — and it builds the dropdown, handles keyboard nav, and tells you what was picked.

  • Shadow DOM
  • JSON items tree
  • Submenus + checkboxes
  • Keyboard navigable

Build the menu from its items tree, set in JavaScript — arrays can’t be HTML attributes. Each item is an action (id + label, optional icon/shortcut), a heading, a separator, a checkbox (add checked), or a submenu (nest more items). Selecting a leaf fires kai-select with its id; checkbox items also report the new checked value.

The trigger is the built-in button — point trigger-icon / trigger-label at it — or your own element in slot="trigger".

<kai-menu trigger-icon="plus" label="Add content"></kai-menu>
<script type="module">
import '@kitn.ai/ui/elements';
const menu = document.querySelector('kai-menu');
menu.items = [
{ heading: true, label: 'Actions' },
{ id: 'add-files', label: 'Add files or photos', icon: 'paperclip', shortcut: '⌘U' },
{ id: 'add-github', label: 'Add from GitHub', icon: 'github' },
{ separator: true },
{ id: 'web-search', label: 'Web search', icon: 'globe', checked: true },
];
menu.addEventListener('kai-select', (e) => {
console.log(e.detail); // { id } — or { id, checked } for a checkbox item
});
</script>

Headings group the list, a submenu nests deeper actions, a separator splits sections, and a checkbox tracks a toggle. This is the composer add-content menu.

A flat set of actions behind a button — the default trigger when you set neither trigger-icon nor trigger-label.

Pair trigger-label with trigger-icon-trailing="chevron-down" for a control that reads like a select — current value on the left, chevron on the right.

The menu builds its own trigger from the trigger-* props. Slot your own element when you need full control of it:

SlotModePurpose
replaceYour own trigger element; replaces the built-in button driven by the `trigger-icon` / `trigger-label` props.
<kai-menu>
<button slot="trigger" aria-label="Open menu">+</button>
</kai-menu>
PropertyTypeDefaultNotes
theme'auto'Color mode (`auto` follows prefers-color-scheme).
itemsTree of menu items. Set as a JS property, not an HTML attribute.
placementOptional placement hint (unused by the underlying Dropdown which always positions bottom-start, kept for future extension).
triggerIconBuilt-in trigger: leading icon (a named icon like `"plus"`, an image URL/data-URI, or text). Use this instead of slotting `slot="trigger"` for the common case; a slotted trigger overrides it.
triggerLabelBuilt-in trigger: a text label (e.g. `"High"`). This is the trigger's VISIBLE text, so it is also its accessible name, and `label` does not override it: an accessible name that does not contain the visible text is unreachable by speech input, which is what WCAG 2.5.3 (Label in Name) exists for. A slotted `slot="trigger"` replaces this built-in trigger entirely and is named differently; see `label`.
triggerIconTrailingBuilt-in trigger: a trailing icon (e.g. `"chevron-down"` for a select look).
labelAccessible name for a trigger with no visible label. Ignored when `triggerLabel` is set, which is already the visible name. It DOES name a slotted `slot="trigger"`, and that is a difference in what the two slots MEAN, not a limitation. `<kai-button>`'s slot IS the button's label, so text slotted there is the name and `label` steps aside. This slot is VISUAL content, a `+` or an `<svg>`, with the name supplied separately: decoration beside a name, never a second name competing with one. So `label` names the trigger here by design. Slotting a real WORD rather than a glyph makes that word a visible label, and an accessible name has to contain the visible text. Then either drop `label` or make it contain the word you slotted.
openDrive/observe open state (Shoelace-style: settable + reflected to the `open` attribute, the menu still self-manages on click/keyboard). Set `el.open = true`, or `<kai-menu open>`; listen for `kai-open-change`.
defaultOpenInitial open state on mount (uncontrolled seed).
disabledDisable the trigger: click/keyboard and `show()` no longer open the menu.
EventDetailNotes
The menu opened or closed (by click, keyboard, Escape, outside-click, or a method).
Fired when the user selects a leaf item. - Plain items: `{ id }`. - Checkbox items: `{ id, checked }` where `checked` is the NEW state. - Radio items: `{ id, radioGroup }`, where the consumer marks `id` as the selected one in `radioGroup` and clears the others.
MethodSignatureNotes
(): voidOpen it programmatically (no-op while disabled).
(): voidClose it programmatically.
(): voidFlip the open state (closes while disabled).

This element wraps these SolidJS components — reach for them directly when you need finer control than the props expose.

DropdownDropdownTriggerDropdownContentDropdownItemDropdownSeparatorDropdownLabelDropdownCheckboxItemDropdownRadioItemDropdownSubDropdownSubTriggerDropdownSubContentKbd