Menus & command pickers
Two interactive pickers cover most of what a chat UI needs. <kai-menu> is a cascading dropdown — the + attach menu, an effort selector, a row-actions menu. <kai-command> is a filterable, grouped picker — an @-mention palette or a command bar. Both are JSON-driven: you set an array, listen for an event. Open the menu, hover Skills, then search the palette below.
Both elements take their data as an items array set in JavaScript — arrays can’t be HTML attributes — and report selections through non-bubbling kai-* events you listen for on the element itself. That’s the whole contract.
The + menu — kai-menu
Section titled “The + menu — kai-menu”Register the elements once, then drop the menu in. The built-in trigger is driven by props: trigger-icon for an icon button, trigger-label for a text button, trigger-icon-trailing for a select-style chevron. label gives an icon-only trigger an accessible name.
<script type="module"> import '@kitn.ai/ui/elements';</script>
<kai-menu trigger-icon="plus" label="Add"></kai-menu>The items tree
Section titled “The items tree”Set items in JavaScript. Each leaf needs an id and a label; the rest is optional. Nest an items array for a submenu, drop in { separator: true } for a rule, { heading: true, label } for a non-interactive group label, checked for a toggle, disabled to grey one out, shortcut for a trailing hint, and icon for a leading glyph (a name from the icon set, an image URL, or text).
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' }, { label: 'Skills', icon: 'sparkles', items: [ { id: 'skill-creator', label: 'skill-creator', icon: 'sparkles' }, { id: 'manage-skills', label: 'Manage skills', icon: 'settings' }, ], }, { separator: true }, { id: 'web-search', label: 'Web search', icon: 'globe', checked: true }, { id: 'coming-soon', label: 'Coming soon', disabled: true },];Reading selections
Section titled “Reading selections”kai-select fires when the user picks a leaf. Plain items carry { id }; checkbox items carry { id, checked }, where checked is the new state. A submenu’s parent doesn’t fire — only the leaf you land on does.
menu.addEventListener('kai-select', (e) => { const { id, checked } = e.detail; if (id === 'web-search') { // checked is the NEW state — write a fresh array back to update the checkmark. menu.items = menu.items.map((item) => item.id === 'web-search' ? { ...item, checked } : item, ); } else { console.log('picked:', id); }});Reassign a new array to update a toggle — mutating an item in place won’t re-render. That’s the same streaming rule as everywhere else in the kit.
A custom trigger
Section titled “A custom trigger”The trigger-icon / trigger-label props cover the common case. When you need your own button, project it into slot="trigger" — a slotted trigger replaces the built-in one.
<kai-menu> <button slot="trigger" aria-label="More">⋯</button></kai-menu>The command palette — kai-command
Section titled “The command palette — kai-command”<kai-command> renders a search box over a grouped list and handles the filtering and keyboard navigation for you. placeholder sets the input text; empty-label is shown when nothing matches.
<kai-command placeholder="Search people, files, commands…" empty-label="Nothing matches"></kai-command>The items list
Section titled “The items list”items is a flat array — no nesting. Each item needs an id and a label; icon and description are optional. Set group and the element buckets items under that heading automatically; items without a group render ungrouped at the top.
const palette = document.querySelector('kai-command');palette.items = [ { id: 'ana', label: 'Ana Ortiz', icon: 'message-circle', description: 'Design', group: 'People' }, { id: 'sam', label: 'Sam Lee', icon: 'message-circle', description: 'Engineering', group: 'People' }, { id: 'design-spec', label: 'design-spec.md', icon: 'file-text', description: '/docs', group: 'Files' }, { id: 'roadmap', label: 'roadmap.md', icon: 'file-text', description: '/docs', group: 'Files' }, { id: 'deploy', label: 'Deploy', icon: 'sparkles', description: 'Ship to production', group: 'Commands' },];Reading selections and the query
Section titled “Reading selections and the query”kai-select fires with { id } on click or Enter. kai-query-change fires { value } on every keystroke — use it to fetch results from a server instead of filtering the static list, the standard move for an @-mention picker.
palette.addEventListener('kai-select', (e) => { console.log('inserted:', e.detail.id);});
palette.addEventListener('kai-query-change', (e) => { // Filtering the static `items` happens for free. Hook this up only when the // results come from a backend. fetchMentions(e.detail.value).then((results) => { palette.items = results; });});- How composition works — slots for position,
::partand tokens for styling, props for per-item data, and when to reach for each. kai-menureference andkai-commandreference — every prop, event, and slot.- Build a composer — the
+menu and effort selector wired into a full Claude-style composer.