Skip to content
kitn AI/UI

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.

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:

fieldtypenotes
typestringBuilt-ins: confirm, choice, form, tasks, link. Extend via types prop.
idstringStable across re-renders; passed back in every policy callback.
titlestringRendered as the card heading.
dataunknownShape depends on type — see each card below.
resolutionCardResolutionSet to re-hydrate the read-only state (e.g. on history load).

CardPolicy callbacks — only supply the ones you need:

callbackwhen 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.

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;
}

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'
}

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;
}>;
}

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];
}
}

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' },
},
];