Environment
Default role

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 &amp; 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 &amp; 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

Environment
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.

Plan

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 &amp; 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 &amp; 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

PropTypeDefaultDescription
orientation"vertical" | "horizontal""vertical"Layout direction.
activeStepnumber—Controlled active step index.
defaultActiveStepnumber0Initial 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.
classNamestring—Additional CSS classes.

Stepper.Step

PropTypeDefaultDescription
errorbooleanfalseMark the step as errored (renders red badge/icon/title).
classNamestring—Additional CSS classes.

Stepper.Header

PropTypeDefaultDescription
iconReactNode—Leading icon slot.
clickablebooleanreached stepsAllow clicking the header to jump to this step. Defaults to true for any step already reached; pass false to lock it.
indicatorReactNode—Override the default numbered/check status badge.
classNamestring—Additional CSS classes.

Stepper.Panel

PropTypeDefaultDescription
classNamestring—Additional CSS classes.

Stepper.Next

PropTypeDefaultDescription
beforeNext() => void | Promise<void>—Work to run before advancing; rejecting keeps the current step.
finishLabelReactNode"Finish"Label shown on the final step.

Accepts all Button props except onClick.

Stepper.Back

PropTypeDefaultDescription
hideOnFirstbooleanfalseHide (instead of disable) on the first step.

Accepts all Button props except onClick.