Skip to content
kitn AI/UI

Build a composer

<kai-prompt-input> ships a textarea and a Send button. Everything else — the + menu, a model switcher, an effort selector, voice buttons, suggestion chips — you compose in through its toolbar slots. Here’s the full Claude-style composer, built from kit pieces. Open the + menu, switch models, pick an effort, or click a starter.

The card exposes three slots — input-top, toolbar-start, and toolbar-end — all inside its shadow boundary. Anything you want above or below the whole card (the notice, the chips) is your own layout: a sibling element, not a slot. We build it in that order.

Register the elements once, then drop the card in. submit="auto" hides the Send button until there’s text; attach={false} (the attach attribute set to false) drops the built-in paperclip so you can supply your own attach action from the + menu.

<script type="module">
import '@kitn.ai/ui/elements';
</script>
<kai-prompt-input
placeholder="How can I help you today?"
attach="false"
submit="auto"
></kai-prompt-input>

Listen for kai-submit on the element itself — kit events don’t bubble. event.detail.value is the flattened text.

const input = document.querySelector('kai-prompt-input');
input.addEventListener('kai-submit', (e) => {
console.log('send:', e.detail.value);
});

toolbar-start is the leading region of the toolbar — the conventional home for a + attach menu. Project a <kai-menu> there. Give it trigger-icon="plus" for the built-in icon button and a label so the icon-only trigger has an accessible name.

<kai-prompt-input placeholder="How can I help you today?" attach="false" submit="auto">
<kai-menu slot="toolbar-start" trigger-icon="plus" label="Add"></kai-menu>
</kai-prompt-input>

The menu’s items is an array, so set it in JavaScript (arrays can’t be HTML attributes). Nest items for a submenu, add { separator: true } for a rule, and checked for a toggle. The kai-select event carries { id } (plus checked for toggle items).

const plus = document.querySelector('kai-menu[slot="toolbar-start"]');
plus.items = [
{ 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: false },
];
plus.addEventListener('kai-select', (e) => {
console.log('menu:', e.detail.id);
});

toolbar-end is the trailing region, just before the Send button. The model switcher, an effort menu, and the two voice buttons live here. Wrap them in a plain <div slot="toolbar-end"> — one slot holds the whole group, and your wrapper lays them out.

<kai-prompt-input placeholder="How can I help you today?" attach="false" submit="auto">
<kai-menu slot="toolbar-start" trigger-icon="plus" label="Add"></kai-menu>
<div slot="toolbar-end" style="display:flex; align-items:center; gap:.25rem">
<kai-model-switcher></kai-model-switcher>
<kai-menu trigger-label="High" trigger-icon-trailing="chevron-down"></kai-menu>
<kai-button variant="subtle" size="icon-sm" icon="mic" label="Voice input"></kai-button>
<kai-button variant="subtle" size="icon-sm" icon="audio-lines" label="Voice mode"></kai-button>
</div>
</kai-prompt-input>

Wire the model switcher and the effort menu the same way — array data in JavaScript, events on the element. The model switcher fires kai-model-change; the effort menu reuses kai-select.

const models = document.querySelector('kai-model-switcher');
models.models = [
{ id: 'opus-4-8', name: 'Opus 4.8' },
{ id: 'sonnet', name: 'Sonnet' },
{ id: 'haiku', name: 'Haiku' },
];
models.currentModel = 'opus-4-8';
models.addEventListener('kai-model-change', (e) => console.log('model:', e.detail));
const effort = document.querySelector('div[slot="toolbar-end"] kai-menu');
effort.items = [
{ heading: true, label: 'Effort' },
{ id: 'high', label: 'High' },
{ id: 'medium', label: 'Medium' },
{ id: 'low', label: 'Low' },
];
effort.addEventListener('kai-select', (e) => console.log('effort:', e.detail.id));

The mic and voice buttons are plain <kai-button>s — variant + icon do the styling, and label is required because they’re icon-only. Listen for kai-click to wire them up. <kai-prompt-input> also has built-in voice and search props if you’d rather use its own toolbar buttons; here we compose our own so they sit in the trailing cluster.

Suggestions and the notice — your own layout

Section titled “Suggestions and the notice — your own layout”

The chips below the card and the notice above it aren’t slots — there’s no region for them inside the card’s shadow boundary. They’re sibling elements in your layout. Stack them around the input:

<div style="display:flex; flex-direction:column; gap:.75rem; max-width:640px">
<kai-notice severity="warning" dismissible>
A scheduled maintenance window starts at 18:00 UTC.
<a slot="action" href="/status">Status</a>
</kai-notice>
<kai-prompt-input placeholder="How can I help you today?" attach="false" submit="auto">
<!-- toolbar-start + toolbar-end slots, as above -->
</kai-prompt-input>
<kai-suggestions variant="outline" size="sm"></kai-suggestions>
</div>

<kai-notice> takes a severity, an optional action slot for a link, and dismissible for the × (which fires kai-dismiss). <kai-suggestions> takes its chips in JavaScript; clicking one fires kai-select.

const suggestions = document.querySelector('kai-suggestions');
suggestions.suggestions = [
{ label: 'Write', icon: 'pencil' },
{ label: 'Learn', icon: 'book-open' },
{ label: 'Code', icon: 'code' },
{ label: 'Life stuff', icon: 'smile' },
];
suggestions.addEventListener('kai-select', (e) => {
document.querySelector('kai-prompt-input').value = e.detail.value ?? e.detail.label;
});