All blocks

Segmented Tabs Stepper

Segmented control where each step is a selectable tab.

Installation

Add it with the shadcn CLI, open it in v0, or read the source.

$ npx shadcn@latest add https://stepperize.com/r/segmented.json
Dependencies
  • @stepperize/react
Requirements
  • React 18 or later
  • Tailwind CSS

Source

import { defineStepper } from "@stepperize/react";
import { useState } from "react";

const { Stepper } = defineStepper([
	{ id: "plan", title: "Plan" },
	{ id: "billing", title: "Billing" },
	{ id: "team", title: "Team" },
]);

export function SegmentedBlock() {
	const [saved, setSaved] = useState(false);

	return (
		<Stepper.Root className="w-full max-w-md rounded-xl border bg-background p-6 shadow-sm">
			{({ stepper }) => (
				<>
					<Stepper.List className="flex gap-1 rounded-lg bg-muted p-1">
						<Stepper.Items>
							{(step) => (
								<Stepper.Item key={step.id} step={step.id} className="flex-1">
									<Stepper.Trigger className="w-full rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors data-[status=active]:bg-background data-[status=active]:text-foreground data-[status=active]:shadow-sm">
										<Stepper.Title />
									</Stepper.Trigger>
								</Stepper.Item>
							)}
						</Stepper.Items>
					</Stepper.List>

					<div className="mt-6 grid min-h-24 place-items-center rounded-lg border bg-muted/30 p-4 text-center text-sm text-muted-foreground">
						{saved ? (
							<div>
								<p className="font-medium text-foreground">Settings saved</p>
								<p className="mt-1 text-xs">
									Plan, billing, and team settings are up to date.
								</p>
							</div>
						) : (
							<Stepper.Content step={stepper.current.id}>
								“{stepper.current.title}” settings.
							</Stepper.Content>
						)}
					</div>

					<Stepper.Actions className="mt-6 flex justify-between">
						<Stepper.Prev className="inline-flex h-9 items-center rounded-lg border bg-background px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50">
							Back
						</Stepper.Prev>
						{saved ? (
							<button
								type="button"
								onClick={() => {
									setSaved(false);
									stepper.reset();
								}}
								className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
							>
								Restart flow
							</button>
						) : stepper.isLast ? (
							<button
								type="button"
								onClick={() => setSaved(true)}
								className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
							>
								Save settings
							</button>
						) : (
							<Stepper.Next className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50">
								Next
							</Stepper.Next>
						)}
					</Stepper.Actions>
				</>
			)}
		</Stepper.Root>
	);
}

When to use it

Use for short flows (3–4 steps) where users benefit from seeing and jumping between all steps at once.

Accessibility

Each segment is a real `Stepper.Trigger` button with `data-status`; active state is conveyed by both color and weight, not color alone.

Customization

Style the active tab via `data-[status=active]`. Replace the pill background with an underline by removing the `bg-background` and adding a bottom border.

Related blocks