Skip to content
kitn AI/UI

Message

kai-message

Brings its own layout for user and assistant turns, so your thread only has to iterate the list.

  • Shadow DOM
  • 1 event
  • User + assistant roles
  • Avatar support
  • Custom actions

Set message in JavaScript (objects can’t be HTML attributes) and listen for kai-message-action:

<kai-message id="msg" style="display:block;"></kai-message>
<script type="module">
import '@kitn.ai/ui/web-components';
await customElements.whenDefined('kai-message');
const msg = document.getElementById('msg');
msg.message = {
id: 'm-1', role: 'assistant',
parts: [
{ type: 'reasoning', text: 'The user wants X, so I should do Y.' },
{ type: 'tool', tool: { type: 'search', state: 'output-available', output: { hits: 3 } } },
{ type: 'text', text: "Here's the plan:\n\n```js\nconst kit = useChat();\n```" },
],
actions: ['copy', 'like', 'dislike', 'regenerate'],
};
msg.addEventListener('kai-message-action', (e) => {
console.log(e.detail.action, e.detail.messageId);
});
</script>
  • message takes the whole object: id, role, an ordered parts array, plus optional actions, avatar, and feedback. There is no scalar shortcut for the text; even one line is a { type: 'text', text } part.
  • Markdown is on by default for role="assistant", off for role="user". Override with the markdown boolean.
  • actions accepts built-in names ('copy', 'like', 'dislike', 'regenerate', 'edit') and/or { id, label, icon?, tooltip? } descriptors, or <kai-action> children in the light DOM.
  • actionsReveal — 'always' (default) keeps the action bar visible; 'hover' hides it until the row is hovered.
Child elementAttributesText contentNotes
actioniconidlabeltooltip
Yes—

The built-in copy, like, and dislike actions are wired: the element handles the interaction, and you listen for kai-message-action.

  • copy writes the message content to the clipboard, flips its icon to a check for a couple of seconds, and raises a “Copied to clipboard” toast.
  • like / dislike mark the chosen vote active (filled, aria-pressed) and animate the other out. Tapping the active vote clears it and brings both back; setting a vote raises a “Thanks for your feedback” toast, and clearing is silent.

They survive streaming: the vote and copied state live above the message list, so a fresh messages array per chunk never wipes a user’s vote.

kai-message-action carries a state field for the toggleable votes — 'on' when a vote is set, 'off' when cleared. Copy and custom actions omit it.

msg.addEventListener('kai-message-action', (e) => {
const { messageId, action, state } = e.detail;
if (action === 'like' || action === 'dislike') {
if (state === 'on') saveVote(messageId, action);
else clearVote(messageId);
}
});

To re-hydrate a persisted vote (on history load, say), set feedback on the message; a controlled feedback wins over the element’s optimistic state:

msg.message = { id: 'm-1', role: 'assistant', parts: [{ type: 'text', text: '…' }],
actions: ['copy', 'like', 'dislike'], feedback: 'like' };

Cleaner in dense threads.

Compose your own message rows: inject a per-message header or footer, or replace the avatar rail (avatar="none" drops it entirely). These are the keystone of a custom message list.

SlotModePurpose
injectA per-message header at the TOP of the body, above reasoning/tools/content: a model-name label, a role + timestamp line.
injectA row at the BOTTOM of the body, below the action bar: a citation/sources row, a token-cost/latency line.
replaceReplaces the built-in avatar rail with your own node. Use `avatar="none"` to omit the rail and let the body span the full row.

Restyle the row, bubble, content, or action bar from outside via ::part.

PartPurposeExample
The message row wrapper (avatar rail + body column). Restyle its gap or alignment from outside.
kai-message::part(row) { gap: 0.75rem }
The content bubble wrapper. Restyle its background, radius, or padding; for a user message this is the rounded chat bubble.
kai-message::part(bubble) { background: var(--color-primary); color: var(--color-primary-foreground) }
The rendered message text/markdown region (same node as `bubble`). Target it to tune typography from outside.
kai-message::part(content) { font-size: 0.9375rem }
The action-bar row (copy / like / regenerate …). Restyle its spacing or hide it entirely from outside.
kai-message::part(actions) { gap: 0.25rem }
The citation row rendered from the message’s `source` parts: a wrapped row of chips below the bubble, never inside it. Restyle its spacing or hide it entirely from outside.
kai-message::part(citations) { gap: 0.5rem }
One attachment item: the chip, row or tile, whichever variant is rendering. Restyle its background, radius or border from outside without caring which layout it is.
kai-chat::part(attachment) { border-radius: 0.25rem }
The attachment’s filename label. Present in every variant that shows one (a grid tile omits it for an image, which is its own label). Retune its type or hide it entirely.
kai-chat::part(attachment-name) { font-size: 0.75rem }
Replaces the built-in avatar rail with your own node. Use `avatar="none"` to omit the rail and let the body span the full row.—
PropertyTypeDefaultNotes
theme'auto'Color mode (`auto` follows prefers-color-scheme).
message—The full message object. Set as a JS property.
role'assistant'Who is speaking. NOT an ARIA role: it renders role="article" with a named aria-label instead, and shadows the ARIA role attribute (see the note above).
markdown—Force markdown on/off. Defaults to on for assistant, off for user.
proseSize'sm'Text/markdown sizing for the message body.
codeTheme'github-dark-dimmed'Shiki theme name used for fenced code blocks in the content.
codeHighlighttrueDisable syntax highlighting for code blocks (no Shiki loads).
actionsReveal'always'Whether the action bar stays visible or appears on pointer-over; visible by default.
avatarSrc—Convenience avatar image URL (used when `message.avatar` is not set).
avatarFallback—Convenience avatar fallback text (used when `message.avatar` is not set).
avatar—Avatar rail mode. `'none'` omits the rail so the body spans the full row; otherwise the built-in avatar or your `slot="avatar"`.
cardTypes—Card type → custom-element tag overrides/additions, merged over the built-ins. JS property: `el.cardTypes`.
cardSchemas—Card-type JSON Schemas keyed by envelope type; validates each card's `data`. JS property: `el.cardSchemas`.
EventDetailNotes
An action button on a message was clicked. `action` is the built-in name or a custom id.
MethodSignatureNotes
(): voidCopy the message content to the clipboard and show the copied check.

This element wraps these SolidJS components — reach for them directly when you need finer control than the props expose.

MessageMessageAvatarMessageBody