A viewport for stacked Message turns (or any content) that sticks to the bottom as content grows—built on use-stick-to-bottom. Thread wraps the scroll root, ThreadContent wraps the scrolling column, and ThreadScrollToBottom shows a control when the user has scrolled away from the bottom.
I'm visiting Chicago for a long weekend—what's worth doing if I've never been?
Start with the lakefront and Millennium Park for an easy first day.
If the weather stays calm, a short architecture boat tour is a nice add-on. Weekend departures fill up, so book a day ahead if you care about the time slot.
I'm into food more than landmarks. Where should I eat?
Reserve one night for classic deep dish, then keep lunches casual in whatever neighborhood you're exploring.
Is public transit enough, or should I rent a car?
The L trains and buses cover most visitor routes without much hassle.
Rent a car only if you plan to leave the city every day. Downtown parking and hotel garages add up, so defaulting to transit usually saves money.
Any packing tip for April weather?
Pack layers and a light rain jacket; spring weather can swing between chilly mornings and warm afternoons.
Perfect. I'll draft a loose plan and see what fits.
Have a great trip!
"use client";import { Message, MessageContent, MessageMarkdown, MessageStack,} from "@/components/nexus-ui/message";import { Thread, ThreadContent, ThreadScrollToBottom,} from "@/components/nexus-ui/thread";const turns = [ { from: "user" as const, text: "I'm visiting Chicago for a long weekend—what's worth doing if I've never been?", }, { from: "assistant" as const, text: "Start with the lakefront and Millennium Park for an easy first day.\n\nIf the weather stays calm, a short architecture boat tour is a nice add-on. Weekend departures fill up, so book a day ahead if you care about the time slot.", }, { from: "user" as const, text: "I'm into food more than landmarks. Where should I eat?", }, { from: "assistant" as const, text: "Reserve one night for classic **deep dish**, then keep lunches casual in whatever neighborhood you're exploring.", }, { from: "user" as const, text: "Is public transit enough, or should I rent a car?", }, { from: "assistant" as const, text: "The **L** trains and buses cover most visitor routes without much hassle.\n\nRent a car only if you plan to leave the city every day. Downtown parking and hotel garages add up, so defaulting to transit usually saves money.", }, { from: "user" as const, text: "Any packing tip for April weather?", }, { from: "assistant" as const, text: "Pack layers and a light rain jacket; spring weather can swing between chilly mornings and warm afternoons.", }, { from: "user" as const, text: "Perfect. I'll draft a loose plan and see what fits.", }, { from: "assistant" as const, text: "Have a great trip!", },] as const;const ThreadDefault = () => { return ( <Thread> <ThreadContent> {turns.map((turn, i) => ( <Message key={i} from={turn.from}> <MessageStack> <MessageContent> <MessageMarkdown>{turn.text}</MessageMarkdown> </MessageContent> </MessageStack> </Message> ))} </ThreadContent> <ThreadScrollToBottom /> </Thread> );};export default ThreadDefault;
Render useChat messages inside Thread by mapping the same messages array you would render with Message alone. Read each UIMessageparts array and join text parts for MessageMarkdown (streaming updates apply as the SDK appends or grows TextUIPart content).
See Prompt Input for a minimal POST /api/chat route with streamText and toUIMessageStreamResponse.
Install the AI SDK
npm install ai @ai-sdk/react @ai-sdk/openai
Create your chat API route
Use the same handler as in the Prompt Input docs: messages: await convertToModelMessages(messages) and return result.toUIMessageStreamResponse().
Map messages to Message inside Thread
Use isTextUIPart from ai so you only aggregate type: "text" segments. Skip system turns unless you surface them deliberately. Assistant messages can also include reasoning, tool, source, and other part types—extend this loop when you need those in the UI.
Root scroll container wrapping StickToBottom. ThreadContent and ThreadScrollToBottom must live under Thread so the scrollable list and jump control share the same stick-to-bottom context.
Optional control that appears when the user has scrolled away from the bottom, so they can jump back to the latest messages. Supports polymorphism via asChild.