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
Preview
Section titled “Preview”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>messagetakes the whole object:id,role, an orderedpartsarray, plus optionalactions,avatar, andfeedback. 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 forrole="user". Override with themarkdownboolean. actionsaccepts 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 element | Attributes | Text content | Notes |
|---|---|---|---|
| <kai-action> | actioniconidlabeltooltip | Yes | — |
The action row
Section titled “The action row”The built-in copy, like, and dislike actions are wired: the element handles the interaction, and you listen for kai-message-action.
copywrites 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/dislikemark 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.
Reading and persisting the vote
Section titled “Reading and persisting the 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' };Examples
Section titled “Examples”Rich Assistant Message
Section titled “Rich Assistant Message”User Message
Section titled “User Message”Avatar and Custom Action
Section titled “Avatar and Custom Action”Actions Reveal on Hover
Section titled “Actions Reveal on Hover”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.
| Slot | Mode | Purpose |
|---|---|---|
| before-body | inject | A per-message header at the TOP of the body, above reasoning/tools/content: a model-name label, a role + timestamp line. |
| after-body | inject | A row at the BOTTOM of the body, below the action bar: a citation/sources row, a token-cost/latency line. |
avatar· ::part | replace | 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. |
Styling
Section titled “Styling”Restyle the row, bubble, content, or action bar from outside via ::part.
| Part | Purpose | Example |
|---|---|---|
| row | The message row wrapper (avatar rail + body column). Restyle its gap or alignment from outside. | |
| bubble | The content bubble wrapper. Restyle its background, radius, or padding; for a user message this is the rounded chat bubble. | |
| content | The rendered message text/markdown region (same node as `bubble`). Target it to tune typography from outside. | |
| actions | The action-bar row (copy / like / regenerate …). Restyle its spacing or hide it entirely from outside. | |
| citations | 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. | |
| attachment | 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. | |
| attachment-name | 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. | |
| avatar | 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. | — |
| Property | Type | Default | Notes |
|---|---|---|---|
| theme | "light" | "dark" | "auto" | 'auto' | Color mode (`auto` follows prefers-color-scheme). |
| message | — | The full message object. Set as a JS property. | |
| role | "user" | "assistant" | '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 | boolean | — | Force markdown on/off. Defaults to on for assistant, off for user. |
| proseSize | "xs" | "sm" | "base" | "lg" | 'sm' | Text/markdown sizing for the message body. |
| codeTheme | string | 'github-dark-dimmed' | Shiki theme name used for fenced code blocks in the content. |
| codeHighlight | boolean | true | Disable syntax highlighting for code blocks (no Shiki loads). |
| actionsReveal | "always" | "hover" | 'always' | Whether the action bar stays visible or appears on pointer-over; visible by default. |
| avatarSrc | string | — | Convenience avatar image URL (used when `message.avatar` is not set). |
| avatarFallback | string | — | Convenience avatar fallback text (used when `message.avatar` is not set). |
| avatar | string | — | Avatar rail mode. `'none'` omits the rail so the body spans the full row; otherwise the built-in avatar or your `slot="avatar"`. |
| cardTypes | Record<string, string> | — | Card type → custom-element tag overrides/additions, merged over the built-ins. JS property: `el.cardTypes`. |
| cardSchemas | Record<string, object> | — | Card-type JSON Schemas keyed by envelope type; validates each card's `data`. JS property: `el.cardSchemas`. |
Events
Section titled “Events”| Event | Detail | Notes |
|---|---|---|
| kai-message-action | | An action button on a message was clicked. `action` is the built-in name or a custom id. |
Methods
Section titled “Methods”| Method | Signature | Notes |
|---|---|---|
| copy | (): void | Copy the message content to the clipboard and show the copied check. |
Composed from
Section titled “Composed from”This element wraps these SolidJS components — reach for them directly when you need finer control than the props expose.