Skip to contents

Message Scroller

A chat transcript that follows new messages, anchors each turn and keeps the reader's place.

What is vernacular architecture?
Building done outside any academic tradition, without professional guidance. It covers a wide range of building types from around the world.
How much of the built world is it?
Amos Rapoport estimated in 1995 that it makes up 95% of the built environment.

Message Scroller ports @shadcn/react’s message scroller. It follows that package’s behavior, not Base UI’s.

Anatomy

Import the component and assemble its parts:

Anatomy

Examples

Anchoring turns

Set scrollAnchor on the row that starts a turn, such as the user’s message. When a new anchored row arrives, the viewport places it near the top, with scrollPreviousItemPeek pixels of the previous row still visible, and holds it there while the reply streams in below. With autoScroll, the viewport follows the end again once the reply fills the space below the anchor.

Avoiding a flash on reload

For the 'end' and 'last-anchor' opening positions, the root and the viewport have data-pending-scroll until the position is applied. Hide the viewport with visibility: hidden while it is set, so the layout still exists for the opening scroll:

Pending scroll

Reading scroll state

getMessageScroller() returns the scroller of the closest provider. Call it while a component initializes. Its scrollable and visibility getters are reactive. Visibility is tracked only while something reads it.

Outline

API reference

Provider

Owns the scroll state and the behavior props, and provides them to the parts and getMessageScroller(). Doesn't render its own HTML element.

autoScroll boolean false
Description

Follow new content at the bottom while the viewport is already at the end.

Type
boolean
Default
false
defaultScrollPosition MessageScrollerDefaultScrollPosition 'end'
Description

Opening position on the first non-empty render, applied once.

Type
MessageScrollerDefaultScrollPosition
Default
'end'
scrollEdgeThreshold number 8
Description

Distance from an edge that still counts as at the start or end.

Type
number
Default
8
scrollMargin number 0
Description

Default margin on the aligned edge for commands and visibility.

Type
number
Default
0
scrollPreviousItemPeek number 64
Description

Extra top margin for a newly anchored row, added to scrollMargin.

Type
number
Default
64

Root

The frame around the viewport. It mirrors the viewport's scroll-state attributes. Renders a <div> element.

This part takes no props beyond its element's HTML attributes.

data-autoscrolling
Present while a programmatic scroll to the end runs.
data-pending-scroll
Present until the 'end' or 'last-anchor' opening position is applied. Hide the viewport with visibility: hidden while it is set.
data-scrollable
The edges the viewport can scroll toward: start, end or start end. Absent when the content fits. Match one edge with [data-scrollable~="end"].

Viewport

The scrollable, focusable transcript region. Renders a <div> element.

preserveScrollOnPrepend boolean true
Description

Keep the first visible messageId row stable when rows are prepended.

Type
boolean
Default
true
data-autoscrolling
Present while a programmatic scroll to the end runs.
data-pending-scroll
Present until the 'end' or 'last-anchor' opening position is applied. Hide the viewport with visibility: hidden while it is set.
data-scrollable
The edges the viewport can scroll toward: start, end or start end. Absent when the content fits. Match one edge with [data-scrollable~="end"].

Content

The transcript log. Every direct child should be a MessageScroller.Item. Renders a <div> element.

spacerClass string —
Description

Class for the internal tail spacer used when anchoring turns near the top.

Type
string

Item

One transcript row: a message, marker, typing row, separator or load-more row. Renders a <div> element.

messageId string —
Description

Stable row id for scrollToMessage, visibility and prepend preservation.

Type
string
scrollAnchor boolean false
Description

Marks a turn boundary that newly appended anchors and last-anchor restore use.

Type
boolean
Default
false
data-message-id
The item's messageId.
data-scroll-anchor
Whether the item is a turn boundary.

Button

Scrolls to the start or end of the transcript. It is inert and out of the tab order while there is nothing to scroll toward. Renders a <button> element.

behavior ScrollBehavior 'smooth'
Description

Native scroll behavior when clicked.

Type
ScrollBehavior
Default
'smooth'
direction MessageScrollerButtonDirection 'end'
Description

Transcript edge to scroll toward.

Type
MessageScrollerButtonDirection
Default
'end'
render Snippet<[props: HTMLButtonAttributes, state: MessageScrollerButtonState]> —
Name
Description

Replaces the default <button>. Spread props onto the host element.

Type
Snippet<[props: HTMLButtonAttributes, state: MessageScrollerButtonState]>
active boolean
Name
active
Description

Whether content is hidden toward this button's direction.

Type
boolean
direction MessageScrollerButtonDirection
Name
direction
Type
MessageScrollerButtonDirection
data-active
Whether the button can scroll toward its edge.