Usage
log region of Message and SystemMessage items, oldest first. It is a thin layer over ChatScroll. ChatScroll owns the scrolling: the transcript stays at the latest message while the reader is there, keeps the viewport still when messages arrive or older ones load, and shows the jump-to-latest control once the reader has moved up. MessageList adds what ChatScroll leaves to its caller: it draws each item, derives each message's place in a run and the space between items, and shows the empty state or placeholder lines when there is nothing to read yet. It draws no bubble or line of its own.
min-block-size: 0 and grow). A list with no bounded height has nothing to scroll.
items is the whole transcript, oldest first. Each item has a stable id, which is its React key, so older messages added above never remount the ones below.
| Kind | props | Drawn with |
|---|---|---|
MessageListItemKind.Message | Message's props except group, variant, as and ref | Message |
MessageListItemKind.System | SystemMessage's props except as and ref | SystemMessage |
MessageListItemKind.Block | author, authorName, children | A wrapper around your content (attachment, tool call, reasoning) |
props. The list never rewrites them.
variant for every message: the plain transcript line (default) or a bubble. selfVariant draws the reader's own messages differently, such as bubbles over plain lines; a transcript has no other split.showAuthor and metaPlacement as defaults for every message. A message that sets its own keeps it.group for each message. A message continues a run when the one before is a message from the same sender (the same side, from author or the from role, and the same name, from authorName or the identity name), and continues it after when the next is. A system line, a block, or a different author or name ends a run. Message uses the position to square the corners of a bubble that face a neighbour.items. The list's own margin is below; ChatScroll's 20px gap is added to it, so the rendered space is 28px and 44px (see the end of this section):
| Between | List margin |
|---|---|
| Two items of the same sender: message to message, message to attachment, message to tool call, reasoning, and back | --space-8 (8px) |
| Any change of sender | --space-24 (24px) |
| Either side of a system line, including a date or unread separator, and including between two items of one sender | --space-24 (24px) |
| Before the first item | none |
author, or the from role when author is omitted (user is Self, any other role Other); the name is authorName, or the name of identity. Two items belong to one sender when both are equal. A System item declares none, so it is always a boundary. There is no sender key or provider to set, and the grouping of bubble corners reads the same sender.
An attachment, a tool call or reasoning that is its own entry in the transcript is a Block item. Give it the same author and authorName as the message it belongs to:
attachments slot is part of that message and does not take this gap.
The gap is derived on every render, so a message from the same sender that arrives joins the run (8px margin, 28px rendered), and a system line inserted into a run puts 44px rendered on both of its sides. It is rendered on the server, so the first paint is final. The spacing is a block-start margin on each item after the first, so measure it as the distance between the rendered boxes.
The transcript has one spacing and no density option. ChatScroll's list adds a 20px gap between items on top of those margins, so the rendered space is 28px within a sender and 44px at a change of sender and around a system line. The values are provisional: the owner has not confirmed whether the gap should add to the 8px and 24px margins or replace them (owner-confirm 12 in the contract). Message and SystemMessage have no block spacing of their own, so stacking them outside MessageList leaves them touching until their container sets a gap.
emptyTitle (and emptyDescription) show an EmptyState inside the log when there are no items and nothing is loading. The log stays mounted, so the first message is announced when it arrives.loading with loadingLabel shows placeholder lines (Skeleton) at the top of the transcript through ChatScroll's loading row, which pauses onReachStart and keeps the log quiet. loadingLabel is announced once, through the live announcer, when loading starts. With no items yet the placeholders replace the empty state. Remove loading in the same update that adds the messages.errorRow is ChatScroll's slot, passed through.overlay is ChatScroll's: it floats a composer over the block end of the transcript, and the list keeps the latest message clear of it. The gutter (24px) and the gap between items (20px) are ChatScroll's single rhythm. A composed chat surface with a composer slot is the ChatPanel block.
label, jumpToLatestLabel, jumpUnreadLabel, unreadCount, onReachStart, onAtEndChange and ref are ChatScroll's and keep its meaning. unreadCount is caller-owned: count the messages that arrived while onAtEndChange last said the reader was away, and reset it when it says they are back.
The list does not format times, parse messages, load history or send. The caller passes items, calls onReachStart to load, and owns the composer beside it.