Elements

Scroll anchor

Streaming never steals your scroll position; a pill offers the way back down.

Why does the thread jump while streaming?
The viewport pins to the bottom only while you are already there, so mid-stream layout shifts never steal your position.
And when I scroll up to reread something?
fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-scroll-anchor"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A scroll anchor keeps a message list pinned to the bottom while it grows, then gets out of the way the moment someone scrolls up to read something older. With a runtime the pin and the jump-back button are the viewport's own behavior; standalone you track the pin state and the unseen count yourself.

Getting started

Let the viewport pin itself

components/assistant-ui/elements/thread.aui.tsx
"use client";

import { ThreadPrimitive } from "@assistant-ui/react";

function Viewport() {
  return (
    <ThreadPrimitive.Viewport className="relative h-64 w-full max-w-sm overflow-hidden rounded-2xl">
      <ThreadPrimitive.Messages>{({ message }) => null}</ThreadPrimitive.Messages>
    </ThreadPrimitive.Viewport>
  );
}

autoScroll defaults to true (unless turnAnchor="top"), so the viewport scrolls to the newest content on its own; a reader who scrolls up interrupts that until they scroll back down themselves.

Add the jump-back pill

import { ThreadPrimitive } from "@assistant-ui/react";
import { ArrowDownIcon } from "lucide-react";

<ThreadPrimitive.ScrollToBottom className="absolute inset-x-0 bottom-3 mx-auto flex w-fit items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs">
  <ArrowDownIcon className="size-3 opacity-60" />
  New messages
</ThreadPrimitive.ScrollToBottom>

ThreadPrimitive.ScrollToBottom renders null on its own while the viewport is already at the bottom, so it needs no manual pinned check. It has no built-in count of how many messages were missed; track that yourself from s.thread.messages.length if you want the "n new messages" label ScrollAnchor shows standalone.

Anatomy

<div data-slot="scroll-anchor">
  <div>{/* viewport: messages, newest last */}</div>
  <button>{/* jump to bottom, only rendered while unpinned */}</button>
</div>

Standalone, appending a message only auto-scrolls while pinned; scrolling away flips pinned to false on the next appended message and the count of unseen messages grows from there. Once two or more messages arrive unseen, it jumps back on its own after 2.4 seconds unless paused; pressing the pill jumps immediately. onSettled only fires when the whole array has landed and the view is still pinned, so it never fires while the reader is scrolled away reading.

Examples

Pausing the feed

There's no equivalent pause on the runtime viewport: new messages always arrive as fast as the model streams them. What you can do instead is drop autoScroll to stop the automatic pin without stopping the stream itself:

<ThreadPrimitive.Viewport autoScroll={false}>
  {/* ... */}
</ThreadPrimitive.Viewport>

Scroll to a specific message instead of the bottom

turnAnchor="top" changes what pinned means: instead of tracking the bottom, the viewport keeps the newest user turn pinned near the top of the visible area, useful for a focused reading layout instead of a classic chat scroll:

<ThreadPrimitive.Viewport turnAnchor="top">
  {/* ... */}
</ThreadPrimitive.Viewport>

API reference

Primitive parts

PartRendersNotes
ThreadPrimitive.ViewportdivScrollable container. autoScroll, turnAnchor, and the scrollToBottomOn* flags control when it pins.
ThreadPrimitive.ScrollToBottombuttonRenders null while already at the bottom. Accepts asChild and a behavior prop ("smooth" | "auto").

Viewport props

PropTypeDefaultDescription
autoScrollbooleantrue (false when turnAnchor="top")Scroll to the bottom automatically as content arrives.
turnAnchor"top" | "bottom""bottom""top" anchors each new user turn near the top instead of pinning the bottom.
scrollToBottomOnRunStartbooleantrueScroll to the bottom when a new run starts.
scrollToBottomOnInitializebooleantrueScroll to the bottom when thread history first loads.
scrollToBottomOnThreadSwitchbooleantrueScroll to the bottom when switching to a different thread.