Use the headless MessageScroller behavior with your own markup and styles.
MessageScroller ships as a headless primitive in the @shadcn/react package.
The package owns all of the scroll behavior, anchoring turns, following streamed
output, preserving the reader's place as history loads, and tracking visibility,
and renders no styles of its own.
The message-scroller.tsx component in the registry is a thin wrapper that adds
Tailwind classes on top. Use the package directly when you want full control over
the markup and styles, or when you are not using the registry.
For the behavior guide and live examples, see the
Message Scroller component.
A scroll container always opens at the top. HTML has no way to set scrollTop,
so a server-rendered transcript shows the oldest messages first. After
JavaScript runs, defaultScrollPosition moves the view, and you see a jump.
When defaultScrollPosition is "end" or "last-anchor", the viewport has
data-pending-scroll until that position is applied. Give the viewport an id
and hide it while the attribute is present. Scope the rule to that id so you do
not hide the root.
If you want "end" visible on first paint, add an inline script right after the
viewport. Using the same id, scroll it to the bottom and remove
data-pending-scroll.
Put the script in your page, not in the primitive. It only works for "end", and
only when the messages are already in the HTML. Add suppressHydrationWarning on
the viewport. If you use a Content Security Policy, pass a nonce.
Do not use this script with "last-anchor". Skip it when messages load on the
client.
The headless root. It owns scroll state and the behavior props, and provides
them to the parts and the hooks. It renders no DOM of its own.
Prop
Type
Default
Description
autoScroll
boolean
false
Follow new content only while the reader is already at the live edge. Wheel, touch, keyboard scroll, and explicit jumps release it.
defaultScrollPosition
"start" | "end" | "last-anchor"
"end"
Opening position on the first non-empty render, applied once. "last-anchor" opens at the last scrollAnchor row and falls back to "end" when the turn fits or no anchor exists. For "end" and "last-anchor", the viewport has data-pending-scroll until the position is applied. See Avoiding a Flash on Reload.
scrollEdgeThreshold
number
8
Distance from either edge that still counts as being at the start or end. Controls state attributes and scroll button visibility.
scrollMargin
number
0
Margin applied to the aligned edge for scrollToMessage, visibility, and programmatic targets.
scrollPreviousItemPeek
number
64
Extra margin added to scrollMargin when a newly appended scrollAnchor item is positioned so part of the previous item stays visible.
The frame and layout container. It fills its parent, so use it inside a
height-constrained layout, within a MessageScroller.Provider.
Prop
Type
Default
Description
...props
React.ComponentProps<"div">
-
Props spread to the frame element.
The root mirrors the scroll-state attributes below (the viewport carries them
too), so you can style the container by scroll state, such as edge fades on the
frame. Headless usage should hide the viewport while data-pending-scroll is
present, using visibility: hidden so layout still exists for the opening
scroll.
Data attribute
Value
Description
data-scrollable
"start" | "end" | "start end" | absent
Edges the viewport can scroll toward. Query one with [data-scrollable~="end"]; absent means it fits.
data-autoscrolling
present
Present while the viewport is programmatically scrolling to the latest message.
data-pending-scroll
present
Present until defaultScrollPosition ("end" or "last-anchor") is applied, or skipped for an empty transcript. Hide the viewport while it is set. See Avoiding a Flash on Reload.
Keep the first visible message item stable when older rows are prepended.
role
string
"region"
Landmark role for the labelled scrollable transcript viewport.
aria-label
string
"Messages"
Accessible name for the scrollable chat transcript.
tabIndex
number
0
Makes the transcript viewport keyboard-scrollable.
...props
React.ComponentProps<"div">
-
Props spread to the viewport element.
Data attribute
Value
Description
data-scrollable
"start" | "end" | "start end" | absent
Edges the viewport can scroll toward. Query one with [data-scrollable~="end"]; absent means it fits.
data-autoscrolling
present
Present while the viewport is programmatically scrolling to the latest message.
data-pending-scroll
present
Present until defaultScrollPosition ("end" or "last-anchor") is applied, or skipped for an empty transcript. Hide the viewport while it is set. See Avoiding a Flash on Reload.
All commands return false when the command could not be applied.
scrollToStart and scrollToEnd return false only when the viewport is not
mounted yet. scrollToMessage returns false when the target is not mounted and
cannot be queued.
Command options:
Option
Type
Default
Description
align
"start" | "center" | "end" | "nearest"
"start"
How a message target aligns in the viewport.
behavior
ScrollBehavior
"auto"
Native scroll behavior for the command.
scrollMargin
number
provider scrollMargin
Margin applied to the aligned edge for this command.
Which edges the viewport can scroll toward, for sibling UI that needs the values
in JavaScript. Prefer the data-scrollable attribute for styling the scroller
itself.
Value
Type
Description
start
boolean
Whether the viewport can scroll toward the start. Content is hidden above (!start means at the top).
end
boolean
Whether the viewport can scroll toward the end. Content is hidden below (!end means at the bottom). Stays false while follow-output is keeping the reader at the live edge.
Visibility state for outline, search, and active-turn UI. It subscribes
separately from useMessageScrollerScrollable, so visibility work is only paid for
when a consumer needs it.
Value
Type
Description
currentAnchorId
string | null
The current anchored turn, based on the last scrollAnchor item at or above the reading line.
visibleMessageIds
string[]
Message ids intersecting the viewport, in document order.
Filter visibleMessageIds in your app when you need a narrower outline, such as
user messages, anchored turns, or search hits.