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 web components 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 web components 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/web-components';</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.