GitHubnpm

Accent theme

Base radius

GitHubnpm

Accent theme

Base radius

Colour scheme

Docs menu

Guides


Primitives
Forms
Layout
Feedback
Overlays
Navigation
Data Display
AI

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.

  1. You
    Can you summarise the release notes?
    09:14
  2. Assistant
    Three changes landed: snap points, a pinned footer, and a linear scrim fade.
  3. Show the full changelog
    Sheet gained snap points and a spring settle, Sheet gained a pinned footer slot, and the scrim now fades linearly so it no longer reads as two separate animations.
  4. Anything else you want pulled out of the notes?
    Answered in 1.2s

Playground

Toggle every adjustable prop and watch the component — and the code — update live.

  1. You
    Can you summarise the release notes?
    09:14
Unstyled

Same component — Kernel look stripped. See Platforms.

author
tone
align
grouped
live
expandable
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>

Props

Props for Message.
PropTypeDefault
author"user" | "assistant" | "system""assistant"
avatarReactNode
nameReactNode
metadataReactNode
groupedbooleanfalse
livebooleanfalse
animateOnMountbooleantrue
Props for Message Bubble.
PropTypeDefault
tone"neutral" | "accent" | "muted" | "danger""neutral"
align"start" | "center" | "end""start"
expandablebooleanfalse
expandLabelReactNode"Show more"
defaultOpenbooleanfalse

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 is aria-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 live marker 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/elements the three hosts aredisplay: contents, so the real<ol>/<li> nest directly and the list keeps its announced item count.