ChatMessageList
Scrollable conversation log with a stick-to-bottom engine — follows streaming content while the reader is at the bottom, breaks off on upward scroll, offers a jump-back pill with a new-message counter, and anchors the scroll position when older history is prepended. Announces generation start and the settled answer to screen readers once, not per token.
Scroll to the bottom and press Append — the list follows. Now scroll up first, then append: following breaks (the badge flips to paused) and the jump-back pill shows how many messages you missed. Click the pill to re-stick. For streaming, tool calls and citations in motion, open the live playground.
01 Examples
Streaming append
Load older history (prepend anchor)
Custom per-message rendering
02 Scroll engine
The list owns one scrolling element and a deterministic engine on top of it — it does not rely on CSS overflow-anchor (Safari has none),
so it corrects the scroll offset itself in every case.
- Follow while at the bottom. New content keeps the viewport pinned to the latest message — ideal for a streaming answer.
- Upward scroll breaks the follow. The moment you scroll away from the bottom the list stops chasing new content and surfaces a floating jump-back pill counting what arrived since.
- Proximity re-stick. Scroll back near the bottom (or click the pill) and
following resumes.
onStickChangefires on every flip. - Prepend anchoring. When older messages are added to the front, the engine holds the reader's current message in place instead of jumping.
Note: rest attributes (including a raw onscroll) land on
the non-scrolling root — observe follow-state through onStickChange, not a scroll listener.
03 Accessibility
Why the log is aria-live="off"
The messages render inside a role="log" region, but
its live channel is deliberately off. A streaming answer changes the DOM
dozens of times a second; a polite/assertive log would fire an announcement on every token
and drown the user in fragments. So the log itself stays silent.
The separate status region
Announcements come instead from a visually hidden role="status" region that speaks only the
meaningful transitions: generatingLabel once when
an assistant message starts streaming, and the settled answer once when it completes (or errorLabel / abortedLabel for a failed stream). Screen-reader users
hear "generating…" then the final answer — never the token-by-token churn.
Scrollable region is focusable
The viewport is a labelled role="region" (listLabel) with tabindex="0", so keyboard users can focus the
conversation and scroll it with the arrow / Page keys. Its focus ring uses focus-visible: (keyboard-only).
The jump-back button
The floating pill is a real <button> whose aria-label carries the pending count (newMessagesLabel) or falls back to scrollToBottomLabel when nothing is pending — so its
purpose is announced, not implied by an icon.
04 API Reference
No matching properties |