Message
A conversation is a real <ol>. Chat messages are ordered — "the third thing said" is meaningful, and reordering them changes the meaning — so the list element that encodes ordering is the right one, and a stack of <div>s throws that away. Each row is an <li> wrapping an<article>, which puts every message in the screen reader's article rotor: readers jump message to message instead of arrowing through every line.
You Can you summarise the release notes?Assistant Three changes landed: snap points, a pinned footer, and a linear scrim fade.Show the full changelog
- Anything else you want pulled out of the notes?
Playground
Toggle every adjustable prop and watch the component — and the code — update live.
You Can you summarise the release notes?
Usage
<MessageList>
<Message
author="user"
name="You"
metadata="09:14"
avatar="Y"
>
<MessageBubble tone="accent" align="start">
Can you summarise the release notes?
</MessageBubble>
</Message>
</MessageList><kernel-message-list>
<kernel-message
author="user"
name="You"
metadata="09:14"
>
<span data-slot="avatar">Y</span>
<kernel-message-bubble tone="accent" align="start">
Can you summarise the release notes?
</kernel-message-bubble>
</kernel-message>
</kernel-message-list>Props
| Prop | Type | Default |
|---|---|---|
author | "user" | "assistant" | "system" | "assistant" |
avatar | ReactNode | — |
name | ReactNode | — |
metadata | ReactNode | — |
grouped | boolean | false |
live | boolean | false |
animateOnMount | boolean | true |
| Prop | Type | Default |
|---|---|---|
tone | "neutral" | "accent" | "muted" | "danger" | "neutral" |
align | "start" | "center" | "end" | "start" |
expandable | boolean | false |
expandLabel | ReactNode | "Show more" |
defaultOpen | boolean | false |
align is independent of author on purpose: a system notice on the assistant's side can still be centred, and a quoted user message can be shown inline. expandablerenders a real <details>, height-animated by the same DetailsPanelAnimator that drives Accordion and Reasoning — not a max-height clamp, so a collapsed message is still real content rather than visually cropped content.
Shape
A bubble reads --kernel-radius-lg, not --kernel-radius-container: it's a text box rather than a padded container, and the container tier — derived as base radius plus a container's padding — rounded a two-line bubble hard enough to look like an accidental pill. Same call, and the same reason, as Composer's.
A bubble that fits on one line gets a real pill instead, because that's what a single-line bubble wants and what every chat surface does; more than one line keeps the large corner, since a stadium around a paragraph looks like a mistake. CSS can't ask how many lines an element rendered on, so a ResizeObserver marks the bubble data-lines="single" or "multi" and the radius follows from there. Expandable bubbles are left out of it: a <details>'s height is its summary plus whatever is disclosed, which says nothing about the message's own line count.
Motion
New rows animate in once, on mount, and never again. That's a plain CSS keyframe animation rather than a JS-driven transition, because "runs once when inserted" is already what a keyframe animation on a fresh element means — re-rendering a long transcript can't re-animate its history. Set animateOnMount to false to opt out, and prefers-reduced-motion removes it entirely.
Accessibility
- Each message's
<article>is labelled with the author's name, which is why the avatar isaria-hidden— a duplicated author reading is noise, not information. - A grouped row keeps the avatar's box and drops only its contents, so messages in a run stay on one text column instead of stepping left.
- The
livemarker renders the word "Writing", not a bare coloured dot: a colour-only status is invisible to anyone who can't see the colour and unreadable to a screen reader. - In
@kernelui-lib/elementsthe three hosts aredisplay: contents, so the real<ol>/<li>nest directly and the list keeps its announced item count.