Generative UI cards
Instead of waiting for the user to type a follow-up, the assistant streams a card directly into the conversation. The user answers it in one click; your CardPolicy receives the result.
How it works
Section titled “How it works”Feed <kai-cards> an array of CardEnvelope objects and a CardPolicy. Each envelope has a type (confirm, choice, form), a stable id, a title, and data that conforms to that type’s schema. When the user interacts, the matching CardPolicy callback fires — no event listeners needed.
import '@kitn.ai/ui/elements';
const cards = document.getElementById('cards');
// Assign the envelope stream in JavaScript — arrays can't be HTML attributes.cards.cards = [/* CardEnvelope objects, one per card type — see below */];
// Wire a CardPolicy — one callback per interaction verb.cards.policy = { onAction: (cardId, action, payload) => console.log('action', cardId, action, payload), onSubmit: (cardId, data) => console.log('submit', cardId, data), onDismiss: (cardId) => console.log('dismiss', cardId), onError: (cardId, message) => console.error('error', cardId, message),};CardEnvelope shape — all card types share the same wrapper:
| field | type | notes |
|---|---|---|
type | string | Built-ins: confirm, choice, form, tasks, link. Extend via types prop. |
id | string | Stable across re-renders; passed back in every policy callback. |
title | string | Rendered as the card heading. |
data | unknown | Shape depends on type — see each card below. |
resolution | CardResolution | Set to re-hydrate the read-only state (e.g. on history load). |
CardPolicy callbacks — only supply the ones you need:
| callback | when it fires |
|---|---|
onAction(cardId, action, payload?) | A confirm button or choice option was submitted |
onSubmit(cardId, data) | A form was submitted; data is the validated values object |
onDismiss(cardId) | The user dismissed a dismissible card |
onError(cardId, message) | The card definition was invalid, or the card failed to render |
Each card type below is live — interact with it and watch its policy events land in the Console beneath the preview.
kai-confirm — ask for a decision
Section titled “kai-confirm — ask for a decision”A confirm card poses a question and offers one or more actions. Tapping an action fires onAction(cardId, action), where action is the button’s id.
{ body?: string; tone?: 'default' | 'warning' | 'danger'; actions: Array<{ id: string; label: string; style?: 'primary' | 'default' | 'destructive'; payload?: unknown; // echoed back in onAction's third arg default?: boolean; // auto-focused when autofocus is set }>; dismissible?: boolean;}kai-choice — pick from options
Section titled “kai-choice — pick from options”A choice card presents a list of options; the user selects one and submits. The chosen option’s id arrives via onAction(cardId, optionId).
{ prompt?: string; options: Array<{ id: string; label: string; description?: string; meta?: string; // trailing label (price, badge…) recommended?: boolean; // renders a "Recommended" pill disabled?: boolean; payload?: unknown; // echoed back in onAction }>; allowOther?: boolean | { label?: string; placeholder?: string }; submitLabel?: string; // defaults to 'Submit'}kai-form — collect structured input
Section titled “kai-form — collect structured input”A form card renders a JSON Schema subset as fields, validates on submit, and delivers the values object through onSubmit(cardId, data). Use x-kai-* hints to pick widgets and control layout.
{ type: 'object'; description?: string; required?: string[]; 'x-kai-submitLabel'?: string; 'x-kai-order'?: string[]; // explicit field order properties: Record<string, { type: 'string' | 'number' | 'boolean' | 'array'; title?: string; enum?: unknown[]; 'x-kai-widget'?: 'textarea' | 'select' | 'radio' | 'slider' | 'rating' | 'switch' | 'checkbox'; 'x-kai-placeholder'?: string; }>;}Streaming cards in as the agent decides
Section titled “Streaming cards in as the agent decides”Append envelopes to the array as the agent generates them — <kai-cards> renders whatever is in the array at that moment:
// Start with nothing; add cards as the model emits them.cards.cards = [];
for await (const event of streamFromYourAgent()) { if (event.type === 'card') { cards.cards = [...cards.cards, event.envelope]; }}Re-hydrating resolved cards
Section titled “Re-hydrating resolved cards”Pass resolution on the envelope to render the read-only (already-answered) state without re-emitting policy events:
cards.cards = [ { type: 'confirm', id: 'confirm-deploy', title: 'Deploy to production?', data: { /* … */ }, resolution: { kind: 'action', action: 'deploy', at: '2026-06-17T09:12:00Z' }, },];Next steps
Section titled “Next steps”kai-cardsreference —typesmap for custom card tags, all props.kai-confirmreference —autofocus,tone, full action shapes.kai-choicereference —allowOther, media options.kai-formreference — full JSON Schema subset + everyx-kai-*hint.- Drop-in chat — wire cards into a streaming conversation with
<kai-chat>.