Elements

Message branches

Navigate between regenerated versions of the same answer without losing your place.

Persist the composer draft in the runtime with the thread id as its key. When the active thread changes, read that value back into the composer.

1 / 3
fig. 01

Installation

npx shadcn@latest add "@assistant-ui/elements-message-branches"
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 regenerated answer does not replace the previous one; it becomes a sibling branch. This element shows the active branch and a n / m stepper to move between siblings. With a runtime the branches come from the thread; standalone you pass them in.

Getting started

Every assistant-ui runtime tracks branches per message. The stepper reads branchNumber and branchCount from the message and switches branches through the runtime, so nothing is stored in component state.

Compose the branch picker

Build the stepper from BranchPickerPrimitive. The primitives own the behavior (which branch is active, disabling at the ends, hiding when there is only one); the element's classes give it this look.

components/assistant-ui/elements/branch-picker.tsx
"use client";

import { BranchPickerPrimitive } from "@assistant-ui/react";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { ghostButton, mono } from "@/components/assistant-ui/elements/surfaces";

export function BranchPicker() {
  return (
    <BranchPickerPrimitive.Root
      hideWhenSingleBranch
      className="flex items-center gap-1"
    >
      <BranchPickerPrimitive.Previous
        aria-label="Show previous response"
        className={cn(ghostButton, "size-6")}
      >
        <ChevronLeftIcon className="size-3.5" />
      </BranchPickerPrimitive.Previous>
      <span className={cn(mono, "text-foreground/35 tabular-nums")}>
        <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
      </span>
      <BranchPickerPrimitive.Next
        aria-label="Show next response"
        className={cn(ghostButton, "size-6")}
      >
        <ChevronRightIcon className="size-3.5" />
      </BranchPickerPrimitive.Next>
    </BranchPickerPrimitive.Root>
  );
}

Place it in the message

The picker must render inside a message scope, so it knows which message's branches to show. Put it in the assistant message's action row, next to copy and regenerate.

components/assistant-ui/elements/thread.aui.tsx
import { MessagePrimitive } from "@assistant-ui/react";
import { BranchPicker } from "./branch-picker";

function AssistantMessage() {
  return (
    <MessagePrimitive.Root>
      <MessagePrimitive.Parts />
      <div className="flex items-center gap-1">
        <BranchPicker />
        {/* copy, regenerate, ... */}
      </div>
    </MessagePrimitive.Root>
  );
}

The Thread element already ships this composition for both user and assistant messages, so installing @assistant-ui/thread gives you branch navigation without any of the above.

Anatomy

<div data-slot="message-branches">
  <p>{/* the active variant, keyed on index so it fades in on change */}</p>
  <div>
    <button aria-label="Show previous response" />
    <span>{/* n / m */}</span>
    <button aria-label="Show next response" />
  </div>
</div>

Standalone, the stepper wraps around: previous on the first variant goes to the last, next on the last goes to the first. With a single variant both buttons render disabled and the counter reads 1 / 1; with no variants it reads 0 / 0. The runtime primitives instead disable at the ends.

Examples

Stepper only

When the message body is already rendered by MessagePrimitive.Parts, render only the stepper. hideWhenSingleBranch removes it from the layout until a second branch exists, which keeps the action row stable.

<BranchPickerPrimitive.Root hideWhenSingleBranch className="flex items-center gap-1">
  <BranchPickerPrimitive.Previous className={cn(ghostButton, "size-6")}>
    <ChevronLeftIcon className="size-3.5" />
  </BranchPickerPrimitive.Previous>
  <span className={cn(mono, "text-foreground/35 tabular-nums")}>
    <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
  </span>
  <BranchPickerPrimitive.Next className={cn(ghostButton, "size-6")}>
    <ChevronRightIcon className="size-3.5" />
  </BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>

Where branches come from

Reloading an assistant message creates a sibling branch and switches to it; editing a user message branches the conversation at that point. A regenerate button next to the picker is all it takes for the stepper to appear:

import { useAui } from "@assistant-ui/react";
import { RefreshCwIcon } from "lucide-react";

function RegenerateButton() {
  const aui = useAui();
  return (
    <button
      type="button"
      aria-label="Regenerate"
      onClick={() => aui.message.reload()}
      className={cn(ghostButton, "size-6")}
    >
      <RefreshCwIcon className="size-3.5" />
    </button>
  );
}

For controls that address a branch directly, switchToBranch steps with { position: "previous" | "next" } or jumps with { branchId } when you hold the message id of a specific branch.

Restyle the stepper

Both lanes take className on the root. The buttons use the shared ghostButton surface and the counter the mono surface from surfaces.tsx, so restyling those two tokens restyles every element that uses them.

<MessageBranches className="max-w-none gap-3" /* ... */ />

API reference

BranchPickerPrimitive

PartRendersNotes
RootdivhideWhenSingleBranch returns null while branchCount <= 1. Must be inside a message scope.
PreviousbuttonDisabled on the first branch. Accepts asChild.
NextbuttonDisabled on the last branch. Accepts asChild.
NumbertextThe 1-based branchNumber.
CounttextThe branchCount.

Message state

SelectorTypeDescription
s.message.branchNumbernumber1-based index of the branch being shown.
s.message.branchCountnumberNumber of sibling branches for this message.
aui.message.switchToBranch(options){ position?: "previous" | "next"; branchId?: string }Switches the shown branch; position steps, branchId jumps to a branch whose message id you hold.