"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
Stepper,
StepperIndicator,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from "@/components/ui/stepper"
const STEPS = [
{ step: 1, title: "Account" },
{ step: 2, title: "Payment" },
{ step: 3, title: "Confirm" },
]
export default function StepperDemo() {
const [step, setStep] = React.useState(2)
return (
<div className="flex w-full max-w-md flex-col gap-8">
<Stepper value={step} onValueChange={setStep}>
{STEPS.map(({ step: s, title }) => (
<StepperItem key={s} step={s}>
<StepperTrigger>
<StepperIndicator />
<StepperTitle>{title}</StepperTitle>
</StepperTrigger>
{s < STEPS.length && <StepperSeparator />}
</StepperItem>
))}
</Stepper>
<div className="flex justify-center gap-2">
<Button
variant="outline"
size="sm"
disabled={step === 1}
onClick={() => setStep(step - 1)}
>
Back
</Button>
<Button
size="sm"
disabled={step === STEPS.length}
onClick={() => setStep(step + 1)}
>
Next
</Button>
</div>
</div>
)
}Installation
npx logic2b@latest add stepperWorking with an AI assistant? Copy this prompt into Claude Code, Cursor or Copilot — it carries the install steps, a no-CLI fallback and a verification checklist.
Add the "stepper" item from the logic2b ui registry (https://ui.logic2b.com) to this project. Follow every step and verify at the end. ## Steps 1. If the project has no `components.json` yet, set up logic2b ui first: ```bash npx logic2b@latest init ``` 2. Install the item (the CLI resolves registry dependencies automatically): ```bash npx logic2b@latest add stepper ``` 3. Install any npm dependencies the command prints. ## If the CLI is not available Do it manually from the registry payload: 1. Fetch https://ui.logic2b.com/r/stepper.json. 2. Write every entry in `files[]` to its mapped project path (see below). 3. Repeat (recursively) for each name in `registryDependencies`. 4. Install every package listed in `dependencies` across those payloads. ## Registry reference - Index of every item: https://ui.logic2b.com/r/index.json - Item payload (full source): https://ui.logic2b.com/r/<name>.json - Usage examples per item: https://ui.logic2b.com/r/demos/index.json → https://ui.logic2b.com/r/demos/<demo>.json - Docs index for agents: https://ui.logic2b.com/llms.txt (full docs: https://ui.logic2b.com/llms-full.txt) - Every docs page is Markdown when you append `.md` to its URL. Registry file paths map to project paths like this: - `ui/*` → `@/components/ui/*` - `blocks/<name>/*` → `@/components/*` - `charts/*` → `@/components/charts/*` - `hooks/*` → `@/hooks/*` - `lib/utils.ts` → `@/lib/utils.ts` - `theme.css` → next to the project's Tailwind entry stylesheet ## Conventions (do not break these) - Tailwind CSS v4 (CSS-first config; no tailwind.config.js needed) and React 19. - All colors go through semantic tokens (`bg-primary`, `text-muted-foreground`, `border-border`, …). Never hardcode hex/oklch values in components. - Dark mode is class-based: toggle `.dark` on `<html>`. - Icons come from lucide-react. ## Verify before finishing 1. The dev server starts and the app renders without console errors. 2. TypeScript compiles (`tsc --noEmit` or the framework's check). 3. Components use the theme: a `<Button>` shows the primary token color, and toggling `.dark` on `<html>` switches the palette.
Install the dependencies:
npm install lucide-reactCopy into ui/stepper.tsx:
"use client"
import * as React from "react"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
/**
* Stepper — a multi-step progress indicator for wizards and onboarding
* flows. Steps derive their state (completed / active / upcoming) from the
* root value; the active step carries aria-current="step".
*/
interface StepperContextValue {
value: number
setValue: (step: number) => void
orientation: "horizontal" | "vertical"
}
const StepperContext = React.createContext<StepperContextValue | null>(null)
const StepItemContext = React.createContext<{
step: number
state: "completed" | "active" | "upcoming"
disabled: boolean
} | null>(null)
function useStepper() {
const ctx = React.useContext(StepperContext)
if (!ctx) throw new Error("Stepper parts must be used within a Stepper.")
return ctx
}
function useStepItem() {
const ctx = React.useContext(StepItemContext)
if (!ctx) throw new Error("StepperItem parts must be used within a StepperItem.")
return ctx
}
function Stepper({
className,
value: valueProp,
defaultValue,
onValueChange,
orientation = "horizontal",
...props
}: Omit<React.ComponentProps<"ol">, "defaultValue"> & {
value?: number
defaultValue?: number
onValueChange?: (step: number) => void
orientation?: "horizontal" | "vertical"
}) {
const [internal, setInternal] = React.useState(defaultValue ?? 1)
const isControlled = valueProp !== undefined
const value = isControlled ? valueProp : internal
const setValue = React.useCallback(
(step: number) => {
if (!isControlled) setInternal(step)
onValueChange?.(step)
},
[isControlled, onValueChange]
)
return (
<StepperContext.Provider value={{ value, setValue, orientation }}>
<ol
data-slot="stepper"
data-orientation={orientation}
className={cn(
"group/stepper flex",
orientation === "horizontal"
? "w-full flex-row items-center"
: "flex-col",
className
)}
{...props}
/>
</StepperContext.Provider>
)
}
function StepperItem({
className,
step,
disabled = false,
...props
}: React.ComponentProps<"li"> & {
step: number
disabled?: boolean
}) {
const { value, orientation } = useStepper()
const state =
step < value ? "completed" : step === value ? "active" : "upcoming"
return (
<StepItemContext.Provider value={{ step, state, disabled }}>
<li
data-slot="stepper-item"
data-state={state}
aria-current={state === "active" ? "step" : undefined}
className={cn(
"group/stepper-item relative flex items-center gap-2",
orientation === "horizontal"
? "flex-1 not-last:pr-2"
: "w-full flex-col items-start not-last:pb-2 [&:not(:last-child)]:min-h-16",
className
)}
{...props}
/>
</StepItemContext.Provider>
)
}
function StepperTrigger({
className,
...props
}: React.ComponentProps<"button">) {
const { setValue } = useStepper()
const { step, disabled } = useStepItem()
return (
<button
type="button"
data-slot="stepper-trigger"
disabled={disabled}
onClick={() => setValue(step)}
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex items-center gap-3 rounded-md text-left outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50",
className
)}
{...props}
/>
)
}
function StepperIndicator({
className,
children,
...props
}: React.ComponentProps<"span">) {
const { step, state } = useStepItem()
return (
<span
data-slot="stepper-indicator"
data-state={state}
className={cn(
"flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-medium transition-colors",
state === "completed" && "bg-primary text-primary-foreground",
state === "active" && "bg-primary text-primary-foreground ring-primary/20 ring-4",
state === "upcoming" && "bg-muted text-muted-foreground",
className
)}
{...props}
>
{children ??
(state === "completed" ? <CheckIcon className="size-3.5" /> : step)}
</span>
)
}
function StepperTitle({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="stepper-title"
className={cn("text-sm leading-tight font-medium", className)}
{...props}
/>
)
}
function StepperDescription({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="stepper-description"
className={cn("text-muted-foreground text-xs leading-tight", className)}
{...props}
/>
)
}
function StepperSeparator({
className,
...props
}: React.ComponentProps<"div">) {
const { orientation } = useStepper()
const { state } = useStepItem()
return (
<div
data-slot="stepper-separator"
data-state={state}
aria-hidden="true"
className={cn(
"bg-border transition-colors",
state === "completed" && "bg-primary",
orientation === "horizontal"
? "mx-2 h-px flex-1"
: "absolute top-8 left-3 h-[calc(100%-2.5rem)] w-px -translate-x-1/2",
className
)}
{...props}
/>
)
}
export {
Stepper,
StepperItem,
StepperTrigger,
StepperIndicator,
StepperTitle,
StepperDescription,
StepperSeparator,
}Usage
import {
Stepper,
StepperIndicator,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from "@/components/ui/stepper"
<Stepper defaultValue={1}>
<StepperItem step={1}>
<StepperTrigger>
<StepperIndicator />
<StepperTitle>Account</StepperTitle>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step={2}>
<StepperTrigger>
<StepperIndicator />
<StepperTitle>Confirm</StepperTitle>
</StepperTrigger>
</StepperItem>
</Stepper>
Each step derives its state from the root value: steps below it are
completed (the indicator swaps to a check), the matching step is active
(and carries aria-current="step"), the rest are upcoming.
API
| Prop | Type | Default |
|---|---|---|
value / defaultValue (Stepper) |
number — controlled/uncontrolled active step |
1 |
onValueChange (Stepper) |
(step: number) => void |
— |
orientation (Stepper) |
horizontal | vertical |
horizontal |
step (StepperItem) |
number — the step this item represents (required) |
— |
disabled (StepperItem) |
boolean — disables the item’s trigger |
false |
children (StepperIndicator) |
ReactNode — override the number/check content |
— |
Examples
Vertical
Set orientation="vertical" for tall flows — separators become connector
lines between the indicators, with room for descriptions.
import {
Stepper,
StepperDescription,
StepperIndicator,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from "@/components/ui/stepper"
const STEPS = [
{
step: 1,
title: "Create your account",
description: "Email, password and workspace name.",
},
{
step: 2,
title: "Invite your team",
description: "Send invites — they can join any time.",
},
{
step: 3,
title: "Install the CLI",
description: "npx logic2b init in your project.",
disabled: true,
},
]
export default function StepperVerticalDemo() {
return (
<Stepper defaultValue={2} orientation="vertical" className="max-w-sm">
{STEPS.map(({ step, title, description, disabled }) => (
<StepperItem key={step} step={step} disabled={disabled}>
<StepperTrigger>
<StepperIndicator />
<span className="flex flex-col gap-0.5">
<StepperTitle>{title}</StepperTitle>
<StepperDescription>{description}</StepperDescription>
</span>
</StepperTrigger>
{step < STEPS.length && <StepperSeparator />}
</StepperItem>
))}
</Stepper>
)
}Accessibility
- Rendered as an ordered list (
<ol>/<li>), so the step count and order are announced natively. - The active step exposes
aria-current="step". - Triggers are real buttons: focusable, disabled when the item is
disabled, and the completed/active/upcoming state rides ondata-statefor styling only. - Separators are
aria-hidden— purely decorative.