VibeUI
ComponentsDocs
VibeUI

Components with taste, that you own. Copy the source, retint the tokens, keep every line.

Library

  • All components
  • Signature pieces
  • Text effects
  • Footers

Docs

  • Introduction
  • Installation
  • Theming
  • CLI

Project

  • GitHub
  • Agents and MCP
  • Issues
  • MIT license
© 2026 VibeUI · MIT licensed
  1. Components
  2. Navigation
  3. Stepper

Stepper

Progress through numbered steps. Circles pop and the connector fills as `current` advances.

NavigationServer-safe
  1. Tokens

  2. 2

    Primitives

  3. 3

    Signature

  4. 4

    Ship

Install

bash
npx @agents-npm-packages/vibeui add stepper --dir ./src/components/ui

Import

tsx
import { Stepper } from "@/components/ui/stepper"

API

Generated from the component's TypeScript types, so it always matches the source. * marks a required prop.

<Stepper>

PropTypeDefaultDescription
steps*{ label: string; description?: string | undefined; }[]—Array of { label, description? }.
current*number—Zero-based index of the active step.
orientation"horizontal" | "vertical""vertical"Layout direction.
classNamestring—Extra classes, merged with the component's own.

Source

src/components/ui/stepper.tsx
import { Check } from "@phosphor-icons/react/dist/ssr";
import { cn } from "@/lib/utils";

/** Progress through numbered steps. Circles pop and the connector fills as `current` advances. */
export function Stepper({
  steps,
  current,
  orientation = "vertical",
  className,
}: {
  steps: { label: string; description?: string }[];
  current: number;
  orientation?: "vertical" | "horizontal";
  className?: string;
}) {
  const horizontal = orientation === "horizontal";

  return (
    <ol
      className={cn(horizontal ? "flex items-start" : "flex flex-col", className)}
    >
      {steps.map((step, i) => {
        const done = i < current;
        const active = i === current;
        const last = i === steps.length - 1;
        return (
          <li
            key={step.label}
            aria-current={active ? "step" : undefined}
            className={cn(
              "relative flex",
              horizontal ? "flex-1 flex-col gap-3 pr-4" : "gap-3.5 pb-7 last:pb-0",
            )}
          >
            {!last ? (
              <span
                aria-hidden
                className={cn(
                  "absolute overflow-hidden bg-line-strong",
                  horizontal
                    ? "left-9 right-2 top-[13px] h-px"
                    : "bottom-1 left-[13px] top-8 w-px",
                )}
              >
                <span
                  className={cn(
                    "absolute inset-0 bg-accent transition-transform duration-500 ease-out",
                    horizontal ? "origin-left" : "origin-top",
                    done
                      ? "scale-100"
                      : horizontal
                        ? "scale-x-0"
                        : "scale-y-0",
                  )}
                />
              </span>
            ) : null}
            <span
              className={cn(
                "relative z-10 flex size-[27px] shrink-0 items-center justify-center rounded-full text-[11px] font-semibold transition-[background-color,border-color,color,transform] duration-300 ease-spring",
                done && "scale-100 bg-accent text-accent-ink",
                active && "scale-110 border-2 border-accent bg-surface text-accent",
                !done && !active && "border border-line-strong bg-surface text-faint",
              )}
            >
              {done ? (
                <Check size={12} weight="bold" className="animate-pop-in" />
              ) : (
                i + 1
              )}
              {active ? (
                <span
                  aria-hidden
                  className="absolute inset-0 animate-ping-soft rounded-full border border-accent opacity-40"
                />
              ) : null}
            </span>
            <div>
              <p
                className={cn(
                  "text-sm font-medium leading-[27px] transition-colors duration-300",
                  active || done ? "text-ink" : "text-muted",
                  horizontal && "leading-snug",
                )}
              >
                {step.label}
              </p>
              {step.description ? (
                <p className="text-[13px] leading-snug text-muted">
                  {step.description}
                </p>
              ) : null}
            </div>
          </li>
        );
      })}
    </ol>
  );
}

Exports Stepper. Peer dependencies: clsx, tailwind-merge, @phosphor-icons/react.

PreviousSidebarNextTable Of Contents