All blocks
Decision Tree
A plan finder that branches to different follow-up questions and converges on a result computed from the answers.
Installation
Add it with the shadcn CLI, open it in v0, or read the source.
$ npx shadcn@latest add https://stepperize.com/r/decision-tree.jsonDependencies
- @stepperize/react
- lucide-react
Requirements
- React 18 or later
- Tailwind CSS
Source
import { defineStepper } from "@stepperize/react";
import { ArrowLeft, Briefcase, RotateCcw, Sparkles, User } from "lucide-react";
const tree = defineStepper([
{ id: "use", title: "Use case" },
{ id: "personal", title: "Budget" },
{ id: "business", title: "Team size" },
{ id: "result", title: "Recommendation" },
] as const);
const { Stepper, useStepper } = tree;
type Use = "personal" | "business";
type Answer = "free" | "paid" | "small" | "large";
// Two branches off the root, each asking a different follow-up, both converging
// on a single "result" step that derives its outcome from the path taken.
const PLANS: Record<string, { name: string; blurb: string }> = {
"personal/free": {
name: "Hobby",
blurb: "Free forever for personal projects.",
},
"personal/paid": {
name: "Pro",
blurb: "For individuals who need more power.",
},
"business/small": { name: "Team", blurb: "Collaboration for small teams." },
"business/large": {
name: "Enterprise",
blurb: "SSO, audit logs, and support.",
},
};
/**
* Decision tree: the first answer branches to one of two follow-up questions,
* then both branches converge on a shared result computed from the answers.
*/
export function DecisionTreeBlock() {
return (
<Stepper.Root className="w-full max-w-sm rounded-xl border bg-background p-6 shadow-sm">
{() => <Inner />}
</Stepper.Root>
);
}
function Inner() {
const stepper = useStepper();
const use = stepper.data.get("use") as Use | undefined;
const answer = stepper.data.get("result") as Answer | undefined;
// Flow data is keyed by step id: the use case lives on "use", the follow-up
// answer on "result" (the step that reads them both back).
const pick = (
key: "use" | "result",
value: string,
next: Parameters<typeof stepper.goTo>[0],
) => {
stepper.data.set(key, value);
stepper.goTo(next);
};
return (
<>
<div className="mb-4 flex items-center gap-2 text-sm font-semibold">
<Sparkles className="size-4 text-primary" /> Find your plan
</div>
<div className="min-h-44">
<Stepper.Content step="use" className="space-y-2">
<p className="text-sm text-muted-foreground">
What are you building?
</p>
<Option
icon={User}
label="A personal project"
onClick={() => pick("use", "personal", "personal")}
/>
<Option
icon={Briefcase}
label="Something for work"
onClick={() => pick("use", "business", "business")}
/>
</Stepper.Content>
<Stepper.Content step="personal" className="space-y-2">
<BackButton onClick={() => stepper.goTo("use")} />
<p className="text-sm text-muted-foreground">What's your budget?</p>
<Option
label="Free only"
onClick={() => pick("result", "free", "result")}
/>
<Option
label="Happy to pay for more"
onClick={() => pick("result", "paid", "result")}
/>
</Stepper.Content>
<Stepper.Content step="business" className="space-y-2">
<BackButton onClick={() => stepper.goTo("use")} />
<p className="text-sm text-muted-foreground">How big is your team?</p>
<Option
label="Under 10 people"
onClick={() => pick("result", "small", "result")}
/>
<Option
label="10 or more"
onClick={() => pick("result", "large", "result")}
/>
</Stepper.Content>
<Stepper.Content step="result">
<Result use={use} answer={answer} onRestart={() => stepper.reset()} />
</Stepper.Content>
</div>
</>
);
}
function Result({
use,
answer,
onRestart,
}: {
use?: Use;
answer?: Answer;
onRestart: () => void;
}) {
const plan = use && answer ? PLANS[`${use}/${answer}`] : undefined;
if (!plan) return null;
return (
<div className="space-y-3 text-center">
<p className="text-xs uppercase tracking-wide text-muted-foreground">
We recommend
</p>
<p className="text-2xl font-bold text-primary">{plan.name}</p>
<p className="text-sm text-muted-foreground">{plan.blurb}</p>
<button
type="button"
onClick={onRestart}
className="inline-flex h-9 items-center gap-1.5 rounded-lg border bg-background px-4 text-sm font-medium transition-colors hover:bg-muted"
>
<RotateCcw className="size-3.5" /> Start over
</button>
</div>
);
}
function Option({
icon: Icon,
label,
onClick,
}: {
icon?: typeof User;
label: string;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className="flex w-full items-center gap-3 rounded-lg border p-3 text-left text-sm font-medium transition-colors hover:border-primary/50 hover:bg-primary/5"
>
{Icon && (
<span className="grid size-8 place-items-center rounded-lg bg-muted text-muted-foreground">
<Icon className="size-4" />
</span>
)}
{label}
</button>
);
}
function BackButton({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="size-3.5" /> Back
</button>
);
}
When to use it
Recommendation quizzes and finders that fork on each answer and converge on a computed outcome.
Accessibility
Only the relevant branch is mounted, so focus order and step count match the visible path.
Customization
Branch with `goTo` (bypasses linear policy) and compute the result from accumulated `stepper.data`.
Related blocks
Conditional OnboardingOnboarding whose path is computed from an earlier answer: team accounts visit the invite step, personal accounts skip it.Plan PickerChoose a plan, set billing, then pay — billing path branches on the plan.AI WorkflowAn iterative generation flow where each run is appended to flow data and Refine loops back to the prompt.