Skip to content

shadcn.io is an independent project built on shadcn/ui

Components

Free

Message Branch for Radix UI

Flip between the versions of a regenerated reply or an edited prompt. For React with Radix UI, with previous and next buttons and a 2 of 3 count.

One message, several versions: the reader sees one at a time and flips between them with the arrows under it, which say "2 of 3".

Use it when a reply is regenerated or a prompt edited and the earlier version should stay: the reader can go back and compare. Each version is a MessageBranchItem around a Message, so the messages keep their own structure. In React, the selector shows only once there are two versions, so a message that was never regenerated looks like any other.

Preview width

Installation

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.

Anatomy

Message Branch anatomy: root, item, selector, previous, page, next
MessageBranch
├── MessageBranchContent
│   └── MessageBranchItem (one per version)
└── MessageBranchSelector
    ├── MessageBranchPrevious
    ├── MessageBranchPage
    └── MessageBranchNext

Usage

import { Message, MessageContent } from "@/components/ui/message";
import {
  MessageBranch, MessageBranchContent, MessageBranchItem,
  MessageBranchNext, MessageBranchPage, MessageBranchPrevious,
  MessageBranchSelector,
} from "@/components/ui/message-branch";
<MessageBranch>
  <MessageBranchContent>
    {versions.map((version) => (
      <MessageBranchItem key={version.id}>
        <Message>
          <MessageContent>{version.text}</MessageContent>
        </Message>
      </MessageBranchItem>
    ))}
  </MessageBranchContent>
  <MessageBranchSelector from="assistant">
    <MessageBranchPrevious />
    <MessageBranchPage />
    <MessageBranchNext />
  </MessageBranchSelector>
</MessageBranch>

Demos

Regenerate

A reply with Copy and Regenerate in its footer, and the selector beside them. It starts with one version, so there is no selector; each Regenerate adds a version and shows it, through branch and onBranchChange. Put the whole message inside MessageBranch when the selector belongs in the message's own footer.

Preview width

Edited Prompts

The person's prompt has two versions, each with the reply it got, so each MessageBranchItem holds a pair. defaultBranch opens on the latest edit, and from="user" puts the selector at the end, under the bubble. Use it when editing a prompt starts the conversation again from there.

Preview width

AI SDK

regenerate replaces a reply with a new one. To keep the old one as a version, save it before you call it, under the prompt it answers.

const { messages, regenerate } = useChat();
const [replaced, setReplaced] = useState<Record<string, UIMessage[]>>({});

function retry(prompt: UIMessage, reply: UIMessage) {
  setReplaced({
    ...replaced,
    [prompt.id]: [...(replaced[prompt.id] ?? []), reply],
  });
  regenerate({ messageId: reply.id });
}

The reply's versions are the saved ones and the reply itself. A new reply has a new id, so keying the branch on it opens it on the newest version.

const versions = [...(replaced[prompt.id] ?? []), reply];

return (
  <MessageBranch defaultBranch={versions.length - 1} key={reply.id}>
    <MessageBranchContent>
      {versions.map((version) => (
        <MessageBranchItem key={version.id}>
          <Message>{/* the version's parts */}</Message>
        </MessageBranchItem>
      ))}
    </MessageBranchContent>
    <MessageBranchSelector from="assistant">
      <MessageBranchPrevious />
      <MessageBranchPage />
      <MessageBranchNext />
    </MessageBranchSelector>
  </MessageBranch>
);

Migrating from AI SDK Elements

AI SDK Elements keeps these parts in components/ai-elements/message.tsx, and an older project in components/ai-elements/branch.tsx as Branch, BranchMessages, BranchSelector, BranchPrevious, BranchNext and BranchPage. Run the command in Installation, then import the parts from @/components/ui/message-branch instead, under their MessageBranch names. What differs:

  • Wrap each version in a MessageBranchItem. Elements counted MessageBranchContent's children; here each version registers itself, so a version can sit in a fragment or a list, and the count is right in Vue, Svelte and Solid too.
  • defaultBranch and onBranchChange are the same; branch makes it controlled.
  • from on MessageBranchSelector is optional: user puts it at the end.
  • The selector is a group of ghost buttons rather than a ButtonGroup, the buttons say "Previous version" and "Next version", and MessageBranchPage is a live region.

FAQ

How do I show the versions of a regenerated reply in React?

Put MessageBranch around them, each version in a MessageBranchItem inside MessageBranchContent, and a MessageBranchSelector with the previous button, the page and the next button. The current version shows; the others stay in the page, hidden, so switching back is instant.

Why does each version need a MessageBranchItem?

So it can register with the branch: the selector counts what registered, in page order, rather than the children it was given. A version can then come from a list, a fragment or a component of your own, and it works the same way in React, Vue, Svelte and Solid.

When does the selector show?

Once there are two versions or more. A reply that was never regenerated has one, so it shows no arrows and no "1 of 1"; the first Regenerate brings the selector in, as in the Regenerate demo.

How do I show the newest version after a regenerate?

Control it: keep branch in state, pass onBranchChange, and set branch to the new version's index when you add it. Or set defaultBranch to the last index and key the branch on the newest reply's id, as the AI SDK section does.

What happens after the last version?

Next goes round to the first, and Previous from the first to the last, as in AI SDK Elements. Both buttons stay enabled while there are two versions or more.

Can the reader switch versions with the keyboard and a screen reader?

Yes. Previous and Next are buttons, named Previous version and Next version, in a group named Versions; Tab reaches them and Enter or Space presses them. The page is a polite live region, so "2 of 3" is read when it changes. The arrows point the other way in a right-to-left page.

Which version shows before the page hydrates?

The first. Versions register in the browser, so the server's HTML shows the first one and no selector; with defaultBranch set, the chosen version takes its place when the page hydrates. A chat rendered in the browser shows the chosen version from its first frame.

API Reference

MessageBranch

PropType
branch?number

The version shown, from 0, when controlled.

defaultBranch?number

The version shown at first, from 0, when uncontrolled.

Default
0
onBranchChange?(branch: number) → void

Called with the version's index, from 0, when the reader moves to it.

Parameters

branch – number

Returns
void
className?string

Merged with the part's classes.

MessageBranchContent

<div> data-slot="message-branch-content"

PropType
className?string

Merged with the part's classes.

MessageBranchItem

<div> data-slot="message-branch-item"

PropType
className?string

Merged with the part's classes.

AttributeType
data-pending?string

Present when pending is true.

data-state?string

"active" when active is true, otherwise "inactive".

MessageBranchSelector

data-slot="message-branch-selector"

PropType
from?"assistant" | "system" | "user"

Whose message it is, as AI SDK Elements takes it: a user's selector sits at the end.

Type
"assistant" | "system" | "user"
className?string

Merged with the part's classes.

AttributeType
data-from?string

Mirrors from.

MessageBranchPrevious

data-slot="message-branch-previous"

PropType
clickEffect?boolean

Apply a click effect to the button.

Default
true
isLoading?boolean

Show a loading indicator.

Default
false
asChild?boolean

Merge props onto the child element instead of rendering a host.

className?string

Merged with the part's classes.

MessageBranchNext

data-slot="message-branch-next"

PropType
clickEffect?boolean

Apply a click effect to the button.

Default
true
isLoading?boolean

Show a loading indicator.

Default
false
asChild?boolean

Merge props onto the child element instead of rendering a host.

className?string

Merged with the part's classes.

MessageBranchPage

<span> data-slot="message-branch-page"

PropType
className?string

Merged with the part's classes.