React Hooks and Instrumentation
Purpose and Scope
This page documents the React package surface that powers shadcn/ui message scrolling without requiring the styled registry component. The main reader problem is a streaming chat transcript: new tokens arrive while a person may be reading older content, selecting text, using the keyboard, loading previous history, or jumping to a specific message. The docs describe this as a scroll problem, not an AI state problem. The package owns anchoring, following streamed output, preserving position during prepends, visibility tracking, and scroll controls while product code continues to own messages, transport, persistence, branching, model state, and presentation.
Sources: apps/v4/content/docs/react/message-scroller.mdx, apps/v4/content/docs/components/base/message-scroller.mdx, apps/v4/content/docs/components/aria/message-scroller.mdx, apps/v4/content/docs/components/radix/message-scroller.mdx
The headless package is useful when the registry wrapper is too opinionated or when an application needs its own markup, classes, and design system integration. The React docs state that the registry component is a thin Tailwind wrapper over the same behavior, while the package exports a namespace object with unstyled parts. That split is important for instrumentation: teams can observe scrollability and visibility, issue scroll commands, preserve keyboard and pointer intent, and still render their transcript with custom message cards, markdown, tool output, avatars, unread markers, or AI SDK message parts.
Sources: apps/v4/content/docs/react/message-scroller.mdx, packages/react/src/message-scroller/components.tsx, packages/react/src/message-scroller/types.ts
Relevant Source Files
- apps/v4/content/docs/react/message-scroller.mdx - Package-level docs for using the headless MessageScroller primitive from @shadcn/react with custom markup and styles.
- apps/v4/content/docs/components/aria/message-scroller.mdx - ARIA component docs that explain the user-experience requirements for streaming chat scroll behavior.
- apps/v4/content/docs/components/base/message-scroller.mdx - Base component docs with the same behavior model and a scoped explanation of what MessageScroller does and does not own.
- apps/v4/content/docs/components/radix/message-scroller.mdx - Radix component docs that confirm the shared behavior guide across styled component families.
- packages/react/src/message-scroller/components.tsx - React implementation for the provider, namespace parts, hooks, contexts, refs, event handlers, and scroll stores.
- packages/react/src/message-scroller/types.ts - Type contracts, defaults, scroll modes, command options, snapshots, and public prop shapes for the message scroller surface.
Core Primitives
The public primitive is organized around a provider plus namespaced parts. A typical tree starts with the provider, then a root frame, viewport, content, individual items, and an optional button. The package docs map the styled registry names to the headless namespace: provider to provider, root scroller to root, viewport to viewport, content to content, item to item, and button to button. That means teams can migrate between a copied registry component and the package primitive without changing the conceptual model; the difference is whether Tailwind classes are supplied by the registry or by the application.
Sources: apps/v4/content/docs/react/message-scroller.mdx, packages/react/src/message-scroller/components.tsx
import {
MessageScroller,
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
} from "@shadcn/react/message-scroller"The provider is the behavioral boundary. In source, it calls the controller with auto-scroll, default opening position, edge threshold, previous-item peek, and scroll margin options, then places both the scroller context and item registration context around its children. The root part installs the root element ref. The viewport installs the viewport element ref, synchronizes after native scroll, records user intent from wheel, touch, and keyboard events, and supports prepend preservation. Items register message identity and anchor metadata so commands and visibility state can refer to stable message identifiers rather than DOM traversal chosen by product code.
Sources: packages/react/src/message-scroller/components.tsx, packages/react/src/message-scroller/types.ts
Hooks, Commands, and Snapshots
The command hook is deliberately small. The implementation reads scrollToEnd, scrollToMessage, and scrollToStart from context and memoizes those functions as the returned command object. Use this hook for buttons, search results, unread markers, route fragment handling, or a transcript outline. The type file defines command options with alignment, native scroll behavior, and an optional scroll margin. Alignment can target the start, center, end, or nearest viewport position, and the provider-level margin becomes the default when a command does not override it.
Sources: packages/react/src/message-scroller/components.tsx, packages/react/src/message-scroller/types.ts
The observable hooks use React external stores instead of local component state. The scrollability hook subscribes to a state store and returns whether the viewport can still scroll toward the start or end. The visibility hook subscribes to a visibility store through observe and unobserve callbacks, returning the current anchored turn and the visible message identifiers in document order. These snapshots let applications render a jump button, disable controls at transcript edges, show out-of-view activity, or update navigation state without coupling those UI decisions to the internal controller implementation.
Sources: packages/react/src/message-scroller/components.tsx, packages/react/src/message-scroller/types.ts
Configuration and Behavioral Defaults
The type contracts capture several edge cases that make streaming transcripts feel stable. The default edge threshold is eight pixels, described as a sub-pixel tolerance so edge detection does not flicker across engines with different scroll rounding. The previous-item peek defaults to sixty-four pixels, keeping context above a newly anchored row. The default scroll margin is zero, while a small epsilon absorbs zoom and HiDPI drift when comparing fractional scroll positions. Programmatic smooth scrolling also marks an autoscrolling state for a short delay before clearing, which helps separate command-driven motion from reader intent.
Sources: packages/react/src/message-scroller/types.ts
The scroll modes explain how the controller thinks about motion. Following bottom means the viewport is pinned to the latest message while auto-scroll is enabled and the reader remains at the live edge. Free scrolling means the reader moved away, so new content and layout changes should not steal the position, although prepended history can still be preserved. Anchored-to-message holds a turn at the reading line while it streams. Settling jump suppresses intent detection during an intentional programmatic jump. These modes match the docs principle: never move the reader against their intent.
Sources: apps/v4/content/docs/components/base/message-scroller.mdx, apps/v4/content/docs/components/aria/message-scroller.mdx, apps/v4/content/docs/components/radix/message-scroller.mdx, packages/react/src/message-scroller/types.ts
Rendering, Refs, and Instrumentation Surface
The implementation is designed to compose with application markup rather than hide it. The component file imports composeRefs, mergeProps, and useRender from the shared use-render module, and the type file imports UseRenderComponentProps. The supplied snippets show those names as part of the message-scroller implementation surface, which is the relevant signal for developers extending the headless parts: refs and props are expected to compose, not replace each other. Viewport refs are especially important because the component must both register its DOM node with the controller and forward the caller's ref.
Sources: packages/react/src/message-scroller/components.tsx, packages/react/src/message-scroller/types.ts
This ref composition is also where instrumentation becomes practical. A custom transcript can attach analytics, focus management, measurement, or testing hooks to its own elements while the provider continues to manage scroll behavior. The viewport forwards native scroll events after synchronizing the controller, and it records wheel, touch, and keyboard activity as deliberate reader intent. The documented user-experience model treats text selection, keyboard use, link opening, search, scrolling, and other interactions as signals that the interface should stop following automatically. The package gives applications places to observe and respond without duplicating the scroll algorithm.
Sources: apps/v4/content/docs/components/base/message-scroller.mdx, packages/react/src/message-scroller/components.tsx
Compact Reference
| Surface | Contract | Notes |
|---|---|---|
| MessageScroller.Provider | children, autoScroll, defaultScrollPosition, scrollEdgeThreshold, scrollPreviousItemPeek, scrollMargin | Owns behavior and state; renders no DOM. |
| MessageScroller.Root | div props | Registers the root frame element. |
| MessageScroller.Viewport | div props plus preserveScrollOnPrepend | Handles native scroll, wheel, touch, keyboard intent, resize, and viewport registration. |
| useMessageScroller | returns scrollToEnd, scrollToMessage, scrollToStart | Use for jump buttons, search, route anchors, and message links. |
| useMessageScrollerScrollable | returns start and end booleans | Use to show whether more content exists above or below. |
| useMessageScrollerVisibility | returns currentAnchorId and visibleMessageIds | Use to highlight the current turn or track visible transcript rows. |
A minimal integration wraps the transcript once, gives each row a stable messageId, and marks user turns or other meaningful rows as scroll anchors. In an AI SDK chat, the docs show messages rendered inside MessageScroller.Item, with a generated message identifier and a scrollAnchor expression based on the message role. The package does not require a specific transport or message schema; the example uses AI SDK message parts only to demonstrate streaming content. Keep transport and persistence outside the scroller, then use the hooks to connect transcript controls back into the viewport.
Sources: apps/v4/content/docs/react/message-scroller.mdx, packages/react/src/message-scroller/types.ts
Next Steps
Use the headless package when you need custom styling or direct instrumentation, and use the styled component docs when you want a registry component with Tailwind classes already applied. For component-family examples, compare the base, ARIA, and Radix message-scroller docs; they share the same behavioral contract while fitting different catalog styles. For AI-specific message production, pair this page with AI chat channels and streaming so the transcript receives stable message rows and incremental content, then return here to wire scroll commands, visibility snapshots, and reader-intent behavior around that stream.