You're all set. Creating your project will provision resources and run the first deployment.
import { Checkbox, Input, InputArea, Radio, Select, Stepper, Text } from "@cloudflare/kumo";
import { BuildingsIcon, GearIcon, RocketLaunchIcon, UsersIcon } from "@phosphor-icons/react";
/**
* Hero demo — a realistic "Create a project" wizard. Each step carries real
* form fields so the layered, accordion-style layout reads the way it would in
* production.
*/
export function StepperHeroDemo() {
return (
<div className="w-full max-w-2xl">
<Stepper.Root>
<Stepper.Step>
<Stepper.Header icon={<BuildingsIcon />}>
Project details
</Stepper.Header>
<Stepper.Panel>
<div className="grid gap-4 sm:grid-cols-2">
<Input label="Project name" placeholder="acme-web" />
<Select
label="Environment"
placeholder="Choose…"
className="w-full"
items={{
production: "Production",
staging: "Staging",
development: "Development",
}}
/>
<div className="sm:col-span-2">
<InputArea
label="Description"
placeholder="What does this project do?"
/>
</div>
</div>
<Stepper.Footer>
<Stepper.Back hideOnFirst />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<UsersIcon />}>Team access</Stepper.Header>
<Stepper.Panel>
<div className="space-y-4">
<Input label="Invite teammates" placeholder="name@company.com" />
<Radio.Group legend="Default role" defaultValue="developer">
<Radio.Item
value="admin"
label="Admin — full access to settings and billing"
/>
<Radio.Item
value="developer"
label="Developer — deploy and manage resources"
/>
<Radio.Item value="viewer" label="Viewer — read-only access" />
</Radio.Group>
</div>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<GearIcon />}>Configuration</Stepper.Header>
<Stepper.Panel>
<div className="flex flex-col items-start gap-3">
<Checkbox label="Enable automatic deployments from main" />
<Checkbox label="Require pull-request reviews" />
<Checkbox label="Enable preview environments" />
</div>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<RocketLaunchIcon />}>
Review & deploy
</Stepper.Header>
<Stepper.Panel>
<div className="rounded-lg bg-kumo-tint p-4">
<Text>
You're all set. Creating your project will provision resources
and run the first deployment.
</Text>
</div>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next finishLabel="Create project" />
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
</Stepper.Root>
</div>
);
}Exploration: progress fill
Temporary, for comparison with the default above. With fill="progress" the
white surface covers every step up to the furthest one reached instead of only
the active step, so it grows as you move through the flow and doesn’t shrink
when you jump back.
Creating your project will provision resources and run the first deployment.
import { Checkbox, Input, Stepper, Text } from "@cloudflare/kumo";
import { BuildingsIcon, GearIcon, RocketLaunchIcon, UsersIcon } from "@phosphor-icons/react";
/**
* Exploration — `fill="progress"`: white fills every step up to the furthest
* one reached, so the surface itself shows how far along you are.
*/
export function StepperProgressFillDemo() {
return (
<div className="w-full max-w-2xl">
<Stepper.Root fill="progress">
<Stepper.Step>
<Stepper.Header icon={<BuildingsIcon />}>
Project details
</Stepper.Header>
<Stepper.Panel>
<Input label="Project name" placeholder="acme-web" />
<Stepper.Footer>
<Stepper.Back hideOnFirst />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<UsersIcon />}>Team access</Stepper.Header>
<Stepper.Panel>
<Input label="Invite teammates" placeholder="name@company.com" />
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<GearIcon />}>Configuration</Stepper.Header>
<Stepper.Panel>
<div className="flex flex-col items-start gap-3">
<Checkbox label="Enable automatic deployments from main" />
<Checkbox label="Enable preview environments" />
</div>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<RocketLaunchIcon />}>
Review & deploy
</Stepper.Header>
<Stepper.Panel>
<Text variant="secondary">
Creating your project will provision resources and run the first
deployment.
</Text>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next finishLabel="Create project" />
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
</Stepper.Root>
</div>
);
}Details
Step 1 of 4
import { Checkbox, Input, Stepper, Text } from "@cloudflare/kumo";
import { BuildingsIcon, GearIcon, RocketLaunchIcon, UsersIcon } from "@phosphor-icons/react";
/** Exploration — `fill="progress"` in horizontal: the rail's pill grows. */
export function StepperProgressFillHorizontalDemo() {
return (
<div className="w-full max-w-2xl">
<Stepper.Root orientation="horizontal" fill="progress">
<Stepper.Step>
<Stepper.Header icon={<BuildingsIcon />}>Details</Stepper.Header>
<Stepper.Panel>
<Input label="Project name" placeholder="acme-web" />
<Stepper.Footer>
<Stepper.Back hideOnFirst />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<UsersIcon />}>Team</Stepper.Header>
<Stepper.Panel>
<Input label="Invite teammates" placeholder="name@company.com" />
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<GearIcon />}>Config</Stepper.Header>
<Stepper.Panel>
<Checkbox label="Enable preview environments" />
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<RocketLaunchIcon />}>Deploy</Stepper.Header>
<Stepper.Panel>
<Text variant="secondary">
Review your settings and create the project.
</Text>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next finishLabel="Create project" />
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
</Stepper.Root>
</div>
);
}Installation
Barrel
import { Stepper } from "@cloudflare/kumo";Granular
import { Stepper } from "@cloudflare/kumo/components/stepper";Usage
Stepper favors composition over configuration. Assemble Header, Panel, and
Footer parts inside each Step rather than driving the component through a
wall of props. The Root owns wizard state — active step, completion, and async
transitions — and lays the steps out in the same layered frame as
LayerDialog: every step sits on the gray frame, and the active step lifts onto
a white card so it’s clear where to focus. Completed steps keep their check,
even after you jump back to an earlier one, and any step you’ve already reached
stays clickable.
<Stepper.Root onComplete={submit}>
<Stepper.Step>
<Stepper.Header icon={<BuildingsIcon />}>Details</Stepper.Header>
<Stepper.Panel>
{/* …content… */}
<Stepper.Footer>
<Stepper.Back hideOnFirst />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
</Stepper.Root>Orientation
Set orientation="horizontal" to render the headers as a connected rail across
the top of the frame. The active step’s panel sits below it as a titled white
card, and a direct Stepper.Footer moves onto the frame beneath that card, the
same place LayerDialog puts its actions. On narrow viewports the rail becomes
a Select that lists every step, with prev/next arrows on either side. The state
machine, async support, and error states are identical — only the layout
changes.
Details
Step 1 of 3
import { Input, Select, Stepper, Text } from "@cloudflare/kumo";
import { BuildingsIcon, RocketLaunchIcon, UsersIcon } from "@phosphor-icons/react";
/**
* Horizontal demo — the same wizard state machine laid out as a top rail with
* the active step's panel below. Just `orientation="horizontal"`.
*/
export function StepperHorizontalDemo() {
return (
<div className="w-full max-w-2xl">
<Stepper.Root orientation="horizontal">
<Stepper.Step>
<Stepper.Header icon={<BuildingsIcon />}>Details</Stepper.Header>
<Stepper.Panel>
<div className="grid gap-4 sm:grid-cols-2">
<Input label="Project name" placeholder="acme-web" />
<Select
label="Environment"
placeholder="Choose…"
className="w-full"
items={{ production: "Production", staging: "Staging" }}
/>
</div>
<Stepper.Footer>
<Stepper.Back hideOnFirst />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<UsersIcon />}>Team</Stepper.Header>
<Stepper.Panel>
<Input label="Invite teammates" placeholder="name@company.com" />
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<RocketLaunchIcon />}>Deploy</Stepper.Header>
<Stepper.Panel>
<div className="rounded-lg bg-kumo-tint p-4">
<Text>Review your settings and create the project.</Text>
</div>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next finishLabel="Create project" />
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
</Stepper.Root>
</div>
);
}<Stepper.Root orientation="horizontal">{/* …steps… */}</Stepper.Root>Async transitions & error states
Mark a step with error to surface failed validation — the badge, icon, and
title turn red. Combine it with beforeNext so a rejected check both stops the
transition and flags the step.
Pass beforeNext to Stepper.Next to run validation or a save before
advancing. The button shows a loading state while the promise is in flight, and
the stepper stays on the current step if it rejects. Use the Root’s
onComplete for the final submit.
We'll check availability before continuing.
Creating your workspace on the pro plan.
import { Input, Select, Stepper, Text } from "@cloudflare/kumo";
import { BuildingsIcon, CreditCardIcon, RocketLaunchIcon } from "@phosphor-icons/react";
import { useState } from "react";
/**
* Async demo — `beforeNext` runs validation/save work before advancing. The
* Next button shows a loading state while the promise is in flight, and the
* stepper stays put if it rejects.
*/
export function StepperAsyncDemo() {
const [name, setName] = useState("");
const [plan, setPlan] = useState<string>("pro");
const [error, setError] = useState<string | null>(null);
const checkAvailability = async () => {
setError(null);
await new Promise((resolve) => setTimeout(resolve, 900));
if (name.trim().length < 3) {
setError("Name must be at least 3 characters.");
throw new Error("validation failed");
}
};
return (
<div className="w-full max-w-2xl">
<Stepper.Root onComplete={() => alert("Workspace created!")}>
<Stepper.Step error={!!error}>
<Stepper.Header icon={<BuildingsIcon />}>
Name your workspace
</Stepper.Header>
<Stepper.Panel>
<div className="space-y-2">
<Input
label="Workspace name"
placeholder="acme"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{error ? (
<Text variant="error">{error}</Text>
) : (
<Text variant="secondary">
We'll check availability before continuing.
</Text>
)}
</div>
<Stepper.Footer>
<Stepper.Back hideOnFirst />
<Stepper.Next beforeNext={checkAvailability}>
Check & continue
</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<CreditCardIcon />}>
Choose a plan
</Stepper.Header>
<Stepper.Panel>
<Select
label="Plan"
value={plan}
onValueChange={(v) => setPlan(v ?? "pro")}
items={{
free: "Free — 1 project",
pro: "Pro — unlimited projects",
enterprise: "Enterprise — SSO & SLA",
}}
/>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next>Continue</Stepper.Next>
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
<Stepper.Step>
<Stepper.Header icon={<RocketLaunchIcon />}>Confirm</Stepper.Header>
<Stepper.Panel>
<div className="rounded-lg bg-kumo-tint p-4">
<Text>
Creating <strong>{name || "your workspace"}</strong> on the{" "}
<strong>{plan}</strong> plan.
</Text>
</div>
<Stepper.Footer>
<Stepper.Back />
<Stepper.Next finishLabel="Create workspace" />
</Stepper.Footer>
</Stepper.Panel>
</Stepper.Step>
</Stepper.Root>
</div>
);
}const validate = async () => {
const ok = await checkAvailability(name);
if (!ok) throw new Error("unavailable"); // stay on this step
};
<Stepper.Next beforeNext={validate}>Check & continue</Stepper.Next>;Controlled
Drive the active step yourself with activeStep + onStepChange, or read
state anywhere inside the Root with the useStepper() hook.
const { activeStep, totalSteps, isLastStep } = useStepper();Props
Stepper.Root
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "vertical" | Layout direction. |
activeStep | number | — | Controlled active step index. |
defaultActiveStep | number | 0 | Initial step for uncontrolled usage. |
onStepChange | (index: number) => void | — | Called when the active step changes. |
onComplete | () => void | Promise<void> | — | Called when Next is pressed on the final step. |
className | string | — | Additional CSS classes. |
Stepper.Step
| Prop | Type | Default | Description |
|---|---|---|---|
error | boolean | false | Mark the step as errored (renders red badge/icon/title). |
className | string | — | Additional CSS classes. |
Stepper.Header
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | Leading icon slot. |
clickable | boolean | reached steps | Allow clicking the header to jump to this step. Defaults to true for any step already reached; pass false to lock it. |
indicator | ReactNode | — | Override the default numbered/check status badge. |
className | string | — | Additional CSS classes. |
Stepper.Panel
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
Stepper.Next
| Prop | Type | Default | Description |
|---|---|---|---|
beforeNext | () => void | Promise<void> | — | Work to run before advancing; rejecting keeps the current step. |
finishLabel | ReactNode | "Finish" | Label shown on the final step. |
Accepts all Button props except onClick.
Stepper.Back
| Prop | Type | Default | Description |
|---|---|---|---|
hideOnFirst | boolean | false | Hide (instead of disable) on the first step. |
Accepts all Button props except onClick.