Skip to main content
Urbicon UI
source

ChatMessageListexperimental

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.

following

Question 1: how does the follow-scroll behave?

Answer 1: while you sit at the bottom the list follows new content. Scroll up and it lets go — a jump-back pill appears with the count of what you missed.

Question 2: how does the follow-scroll behave?

Answer 2: while you sit at the bottom the list follows new content. Scroll up and it lets go — a jump-back pill appears with the count of what you missed.

Question 3: how does the follow-scroll behave?

Answer 3: while you sit at the bottom the list follows new content. Scroll up and it lets go — a jump-back pill appears with the count of what you missed.

Question 4: how does the follow-scroll behave?

Answer 4: while you sit at the bottom the list follows new content. Scroll up and it lets go — a jump-back pill appears with the count of what you missed.

Question 5: how does the follow-scroll behave?

Answer 5: while you sit at the bottom the list follows new content. Scroll up and it lets go — a jump-back pill appears with the count of what you missed.

Question 6: how does the follow-scroll behave?

Answer 6: while you sit at the bottom the list follows new content. Scroll up and it lets go — a jump-back pill appears with the count of what you missed.

Layout
Loading...
Loading syntax highlighting...

01 Examples

Streaming append

Appending to the messages array (or growing the last message's text part as chunks arrive) is all it takes — the list follows as long as the reader is at the bottom. onStickChange tells you when following breaks so you can reflect it in your own UI, e.g. a 'following / paused' badge.
Loading...
Loading syntax highlighting...

Load older history (prepend anchor)

Prepending older messages when the reader hits the top would normally yank the viewport. The list detects the prepend and anchors the scroll position, so the message you were reading stays put while history grows above it.
Loading...
Loading syntax highlighting...

Custom per-message rendering

The message snippet overrides how each entry renders — it receives message, index and isLast, and draws whatever you like. Reach for it to inject date separators, custom system notices, or a bespoke bubble; drop back to the default ChatMessage for the rest.
Loading...
Loading syntax highlighting...

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. onStickChange fires 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

23 props
23 props 1 required

No matching properties

No matching properties

05 Installation

Import

Loading...
Loading syntax highlighting...