Branch picker
Steps between alternative versions of a reply, like 2 / 3, with a crossfade.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/branch-picker.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/branch-picker.json
Usage & API
Example
import { BranchPicker } from "@/components/ai/branch-picker"
<BranchPicker
index={1}
count={1}
onChange={() => {}}
/>Props BranchPicker
| Prop | Type & description | Required |
|---|---|---|
index | number | Required |
count | number | Required |
onChange | (index: number) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Default exports BranchPicker and BranchedReply. The chevron buttons are 28 pixels, with a ring on focus and a slight press scale. Disabled buttons drop their pointer events and fade. The count label is tabular numbers so 1 / 3 and 2 / 3 do not jitter. The live region is polite, which means a quick triple-click may coalesce announcements. Arrow keys are on the group's keydown, and they do not preventDefault, so if the group is inside a scrollable region you should check that the page does not also move. The crossfade uses AnimatePresence with the index as the key. initial is false on the presence wrapper, so the first paint does not fade in from nothing. Only a later index change fades. The demo's user bubble, Should I rent or buy when I move to a new city?, is outside BranchedReply. Put your question above it the same way.
Choosing a layout
Use it when a reply was regenerated and you still have the earlier text. Message actions can fire onRegenerate, and your parent can push the old children and the new children into the versions array, then set index to the latest. Streaming text should finish, or be swapped in as the node for the active index, before you rely on the fade. A caret inside a fading grid cell is a lot of motion at once. Compact and document layouts from Message can be the nodes. The picker does not care which layout you used.
Do not use it as a history of the whole thread. It is one reply, with count versions. Thread navigation is a different control. Do not use it for the person's own edited message. Edit message replaces the user text and tells you that replies after it will be replaced. That is a resend, not a branch of the assistant turn. If you only have one version, you can hide the picker. The component will still render 1 / 1 with both buttons disabled if you mount it.
Does Next ask the model for another answer?
No. Next calls onChange with index + 1, and only if that index exists. Previous does the same downward. The versions are whatever nodes you passed to BranchedReply. To create a branch, generate the text elsewhere, append a node, and move index. Message actions is the regenerate button people already expect. This picker only shows the ones you kept. count on BranchPicker should match versions.length. BranchedReply passes versions.length as that count.
The tall answer and the short answer swap, and the page jumps at the end of the fade.
During the fade both nodes occupy the same grid cell, so the cell's height follows the grid's layout rules while both are mounted. When the exit finishes, the old node is gone and the height snaps to the remaining one. The 0.28 second transition is opacity only, not height. If that snap is a problem, give the wrapper a min height for the longest branch, or fade only after the new node has been measured. The component does not do that for you.