Bloom Logo

Stepper

A progress indicator component for guiding users through a multi-step sequence, supporting custom icons, error states, and direct step clicking.

stepper.tsx

Stepper component with step indicators, completed state checkmarks, error indicators, custom icons, and horizontal/vertical orientations.

ReactTailwindUI ComponentNavigationStepper
"use client";

import * as React from "react";
import { Icon } from "@iconify/react";
import { cn } from "@/lib/utils";

export interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {
  orientation?: "horizontal" | "vertical";
  activeStep?: number;
  onStepClick?: (step: number) => void;
  variant?: "default" | "circle" | "line";
}

const StepperContext = React.createContext<{
  activeStep: number;
  orientation: "horizontal" | "vertical";
  onStepClick?: (step: number) => void;
  variant?: "default" | "circle" | "line";
}>({
  activeStep: 0,
  orientation: "horizontal",
  variant: "default",
});

const Stepper = React.forwardRef<HTMLDivElement, StepperProps>(
  (
    {
      className,
      orientation = "horizontal",
      activeStep = 0,
      onStepClick,
      variant = "default",
      children,
      ...props
    },
    ref
  ) => {
    return (
      <StepperContext.Provider value={{ activeStep, orientation, onStepClick, variant }}>
        <div
          ref={ref}
          className={cn(
            "flex w-full",
            orientation === "horizontal"
              ? "flex-row items-center justify-between"
              : "flex-col space-y-4",
            className
          )}
          {...props}
        >
          {children}
        </div>
      </StepperContext.Provider>
    );
  }
);
Stepper.displayName = "Stepper";

export interface StepperItemProps extends React.HTMLAttributes<HTMLDivElement> {
  step: number;
  isCompleted?: boolean;
  isDisabled?: boolean;
  isError?: boolean;
}

const StepperItem = React.forwardRef<HTMLDivElement, StepperItemProps>(
  (
    { className, step, isCompleted, isDisabled, isError, children, onClick, ...props },
    ref
  ) => {
    const { activeStep, orientation, onStepClick } = React.useContext(StepperContext);
    const isActive = activeStep === step;
    const completed = isCompleted ?? activeStep > step;
    const isClickable = Boolean(onStepClick) && !isDisabled;

    const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
      if (onClick) onClick(e);
      if (isClickable && onStepClick) {
        onStepClick(step);
      }
    };

    return (
      <div
        ref={ref}
        data-state={isError ? "error" : completed ? "completed" : isActive ? "active" : "inactive"}
        onClick={handleClick}
        className={cn(
          "flex items-center gap-3 relative transition-colors",
          orientation === "horizontal" ? "flex-1 last:flex-none" : "w-full",
          isClickable && "cursor-pointer group",
          isDisabled && "opacity-50 pointer-events-none",
          className
        )}
        {...props}
      >
        {children}
      </div>
    );
  }
);
StepperItem.displayName = "StepperItem";

export interface StepperIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
  step: number;
  icon?: string;
  isError?: boolean;
}

const StepperIndicator = React.forwardRef<HTMLDivElement, StepperIndicatorProps>(
  ({ className, step, icon, isError, children, ...props }, ref) => {
    const { activeStep } = React.useContext(StepperContext);
    const isActive = activeStep === step;
    const isCompleted = activeStep > step;

    return (
      <div
        ref={ref}
        className={cn(
          "flex size-9 shrink-0 items-center justify-center rounded-full border text-xs font-semibold transition-all duration-300 select-none",
          isError
            ? "border-danger bg-danger/10 text-danger ring-4 ring-danger/15"
            : isCompleted
            ? "border-primary bg-primary text-primary-foreground shadow-xs"
            : isActive
            ? "border-primary bg-primary/10 text-primary ring-4 ring-primary/15"
            : "border-border bg-muted/50 text-muted-foreground",
          className
        )}
        {...props}
      >
        {isError ? (
          <Icon icon="hugeicons:alert-circle" className="size-4 text-danger" />
        ) : isCompleted ? (
          <Icon icon="hugeicons:tick-02" className="size-4 stroke-[3]" />
        ) : icon ? (
          <Icon icon={icon} className="size-4" />
        ) : (
          children ?? step + 1
        )}
      </div>
    );
  }
);
StepperIndicator.displayName = "StepperIndicator";

const StepperTitle = React.forwardRef<
  HTMLHeadingElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h4
    ref={ref}
    className={cn(
      "text-xs font-semibold text-foreground tracking-tight leading-tight group-hover:text-primary transition-colors",
      className
    )}
    {...props}
  />
));
StepperTitle.displayName = "StepperTitle";

const StepperDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn("text-[11px] text-muted-foreground leading-tight mt-0.5", className)}
    {...props}
  />
));
StepperDescription.displayName = "StepperDescription";

const StepperSeparator = ({ className }: React.HTMLAttributes<HTMLDivElement>) => {
  const { orientation } = React.useContext(StepperContext);

  if (orientation === "vertical") {
    return <div className={cn("ml-4 my-1 w-px h-6 bg-border", className)} />;
  }

  return <div className={cn("flex-1 mx-3 h-px bg-border", className)} />;
};
StepperSeparator.displayName = "StepperSeparator";

export {
  Stepper,
  StepperItem,
  StepperIndicator,
  StepperTitle,
  StepperDescription,
  StepperSeparator,
};

Clickable Icon Stepper

Pass onStepClick to allow users to jump directly to completed or active steps, and pass custom icons to StepperIndicator.

Account Info

Personal details

Payment Setup

Credit card info

Confirmation

Review and submit

Error State Handling

Pass isError to StepperItem and StepperIndicator to indicate validation issues on a specific step.

isError: boolean

Account Info

Verified

Payment Error

Invalid card number

3

Confirmation

Pending

Vertical Stepper

Set orientation to vertical for vertical step layouts.

orientation: horizontal | vertical

Order Placed

July 28, 2026

Processing

Preparing items

Shipped

On the way

API Reference

Props — Stepper

Sub-components for building accessible progress step flows.

ComponentPropsDescription
StepperactiveStep?: number, onStepClick?: (step: number) => void, orientation?: 'horizontal'|'vertical'Root container managing active step index and layout axis.
StepperItemstep: number, isCompleted?: boolean, isError?: booleanIndividual step container.
StepperIndicatorstep: number, icon?: string, isError?: booleanCircle badge displaying step number, icon, or error alert.
StepperSeparatorclassName?: stringConnecting divider line between adjacent steps.