Thread
Arrange a scrollable conversation.
View code
import { Thread } from "@stack/ui/thread";import { Message } from "@stack/ui/message";
export default function Example() { return ( <div className="h-80"> <Thread.Root defaultScrollPosition="start"> <Thread.Scroller> <Thread.Messages> <Thread.Content> {Array.from({ length: 12 }, (_, index) => ( <Thread.Item key={index} messageId={`message-${index}`}> <Message.Root> <Message.Content> <Message.Text parts={[ { type: "text", text: `Message ${index + 1}: Keep related updates together.`, }, ]} /> </Message.Content> </Message.Root> </Thread.Item> ))} </Thread.Content> </Thread.Messages> <Thread.ScrollButton /> </Thread.Scroller> </Thread.Root> </div> );}import { Thread } from "@stack/ui/thread";import { Message } from "@stack/ui/message";Give the consumer-owned wrapper a height so the thread can scroll. Use stable message IDs and ScrollRestoration when switching conversations.
Restore a conversation
Section titled “Restore a conversation”Scroll to an older message, switch conversations, then return. The host saves positions and supplies older pages; Thread.ScrollRestoration restores the saved anchor and reports when the conversation is ready. Grow the reply to check that reading an older message stays paused.
Restoring conversation
View code
import { useRef, useState, type RefObject } from "react";import { Thread, type ThreadScrollPosition } from "@stack/ui/thread";import { Button } from "@/components/ui/button";
function Conversation({ id, positions,}: { id: string; positions: RefObject<Map<string, ThreadScrollPosition>>;}) { const viewport = useRef<HTMLDivElement>(null); const [position] = useState(() => positions.current.get(id)); const [first, setFirst] = useState(40); const [ready, setReady] = useState(false); const [reply, setReply] = useState(""); return ( <div className="h-96"> <Thread.Root> <div className="flex flex-wrap items-center gap-2 py-3"> <Button variant="outline" disabled={first === 0} onClick={() => setFirst((value) => Math.max(0, value - 20))} > Load older messages </Button> <Button variant="outline" onClick={() => setReply((value) => value + "More of the reply. ".repeat(20)) } > Grow reply </Button> <p role="status"> {ready ? `Conversation ${id}` : "Restoring conversation"} </p> </div> <div className="min-h-0 flex-1"> <Thread.Scroller> <Thread.Messages ref={viewport}> <Thread.Content> {Array.from( { length: 60 - first }, (_, offset) => first + offset, ).map((number) => ( <Thread.Item key={number} messageId={`${id}-${number}`}> <div className="min-h-24 py-4"> Message {id}-{number} {number === 59 && <p>{reply}</p>} </div> </Thread.Item> ))} </Thread.Content> </Thread.Messages> <Thread.ScrollButton /> <Thread.ScrollRestoration scopeKey={id} viewportRef={viewport} position={position} onPositionChange={(value) => positions.current.set(id, value)} onReady={() => setReady(true)} cursor={first > 0 ? String(first) : null} loadOlder={async (signal) => { if (signal.aborted) return null; setFirst((value) => Math.max(0, value - 20)); return {}; }} /> </Thread.Scroller> </div> </Thread.Root> </div> );}
export default function Example() { const positions = useRef(new Map<string, ThreadScrollPosition>()); const [conversation, setConversation] = useState("A"); return ( <div> <nav className="flex gap-2" aria-label="Example conversations"> {["A", "B"].map((id) => ( <Button key={id} variant={conversation === id ? "default" : "outline"} onClick={() => setConversation(id)} > Conversation {id} </Button> ))} </nav> <Conversation key={conversation} id={conversation} positions={positions} /> </div> );}Reference
Section titled “Reference”Tables list component-specific props and inherited control options. Components that forward native props also accept the applicable React HTML attributes and event handlers, including data-* and aria-* attributes.
Thread.Root
Section titled “Thread.Root”Also accepts native element props.
| Prop | Type | Required | Description |
|---|---|---|---|
className |
string |
No | Additional CSS classes for the underlying element. |
children |
ReactNode |
No | Content rendered inside the component. |
autoScroll |
boolean |
No | Follow the end of the conversation automatically. Default: true. |
defaultScrollPosition |
MessageScrollerDefaultScrollPosition |
No | Initial conversation scroll position. Default: "end". |
scrollEdgeThreshold |
number |
No | Distance from an edge used to detect whether it is reached. |
scrollPreviousItemPeek |
number |
No | Amount of the previous item visible at a scroll anchor. |
scrollMargin |
number |
No | Space around the scroll target. |
Thread.Scroller
Section titled “Thread.Scroller”Also accepts native element props.
| Prop | Type | Required | Description |
|---|---|---|---|
className |
string |
No | Additional CSS classes for the underlying element. |
children |
ReactNode |
No | Content rendered inside the component. |
Thread.Messages
Section titled “Thread.Messages”Also accepts native element props.
| Prop | Type | Required | Description |
|---|---|---|---|
className |
string |
No | Additional CSS classes for the underlying element. |
children |
ReactNode |
No | Content rendered inside the component. |
preserveScrollOnPrepend |
boolean |
No | Preserve the viewport when earlier content is prepended. |
Thread.Content
Section titled “Thread.Content”Also accepts native element props.
| Prop | Type | Required | Description |
|---|---|---|---|
className |
string |
No | Additional CSS classes for the underlying element. |
children |
ReactNode |
No | Content rendered inside the component. |
spacerClassName |
string |
No | CSS classes for the scroll spacer. |
Thread.Item
Section titled “Thread.Item”Also accepts native element props.
| Prop | Type | Required | Description |
|---|---|---|---|
className |
string |
No | Additional CSS classes for the underlying element. |
children |
ReactNode |
No | Content rendered inside the component. |
messageId |
string |
No | Stable message identifier. |
scrollAnchor |
boolean |
No | Use this message as a scroll anchor. |
Thread.ScrollButton
Section titled “Thread.ScrollButton”Also accepts native element props.
| Prop | Type | Required | Description |
|---|---|---|---|
children |
ReactNode |
No | Content rendered inside the component. |
render |
(ReactElement<unknown, string | JSXElementConstructor<any>> | ((props: Record<string, unknown>, state: { active: boolean; direction: "start" | "end"; }) => ReactElement | null)) |
No | Replace the underlying element using render composition. |
behavior |
ScrollBehavior |
No | Scroll behavior. |
direction |
("start" | "end") |
No | Direction of the scroll action. |
variant |
"link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null |
No | Appearance variant. |
size |
"default" | "sm" | "xs" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null |
No | Size variant. |
Thread.ScrollRestoration
Section titled “Thread.ScrollRestoration”| Prop | Type | Required | Description |
|---|---|---|---|
scopeKey |
string |
Yes | Identity of the current conversation or workspace. |
viewportRef |
RefObject<HTMLElement | null> |
Yes | Ref to the scrollable viewport. |
position |
ThreadScrollPosition | null |
No | Read once per scope. Subsequent saves must not restart restoration. |
onPositionChange |
(position: ThreadScrollPosition) => void |
Yes | Save the current conversation scroll position. |
onReady |
(() => void) |
No | Called when restoration has completed. |
loading |
boolean |
No | Whether data is loading. Default: false. |
connectionState |
"connecting" | "connected" | "disconnected" |
No | Current connection state used during scroll restoration. Default: "connected". |
cursor |
string | null |
No | Cursor identifying earlier available history. Default: null. |
loadOlder |
((signal: AbortSignal) => Promise<unknown | null>) |
No | Update the host’s messages/cursor; return null if no page was loaded. |
Thread.Footer
Section titled “Thread.Footer”Also accepts native element props.
| Prop | Type | Required | Description |
|---|---|---|---|
className |
string |
No | Additional CSS classes for the underlying element. |
children |
ReactNode |
No | Content rendered inside the component. |
Thread.DateSeparator
Section titled “Thread.DateSeparator”Also accepts native element props.
| Prop | Type | Required | Description |
|---|---|---|---|
className |
string |
No | Additional CSS classes for the underlying element. |
children |
ReactNode |
No | Content rendered inside the component. |
dateTime |
string |
No | Machine-readable timestamp for the time element. |