react ~4 KB 0 deps v0.3.1 ↗ GitHub ↗

@mshafiqyajid/react-stepper

Multi-step wizard / stepper. Headless hook + styled component. Linear or non-linear progression, validation gates (sync or async), horizontal or vertical layout, controlled or uncontrolled state. Zero dependencies, fully typed.

Playground #

Props
TSX
import { StepperStyled } from "@mshafiqyajid/react-stepper/styled";
import "@mshafiqyajid/react-stepper/styles.css";

<StepperStyled
  steps={steps}
  onFinish={() => /* submit */}
/>

Install #

npm install @mshafiqyajid/react-stepper

Quick start #

import { StepperStyled } from "@mshafiqyajid/react-stepper/styled";
import "@mshafiqyajid/react-stepper/styles.css";

<StepperStyled
  steps={[
    { id: "account", label: "Account", description: "Email + password" },
    { id: "billing", label: "Billing", validate: () => valid || "Add a payment method" },
    { id: "review",  label: "Review" },
  ]}
  content={{
    account: <AccountForm />,
    billing: <BillingForm />,
    review:  <ReviewSummary />,
  }}
  onFinish={() => submit()}
/>

Async validation #

{
  id: "username",
  label: "Pick username",
  validate: async () => {
    const taken = await checkAvailability(username);
    return taken ? "That username is taken" : true;
  }
}

The hook tracks isPending while the promise resolves; the styled footer auto-disables Back / Next during pending.

Headless #

import { useStepper } from "@mshafiqyajid/react-stepper";

const stepper = useStepper({ steps, mode: "linear" });

return (
  <>
    {steps.map((s, i) => (
      <button
        key={s.id}
        onClick={() => stepper.goTo(i)}
        aria-current={stepper.activeStep === i ? "step" : undefined}
      >
        {s.label}
      </button>
    ))}
    <div>{content[stepper.activeStepId]}</div>
    <button onClick={() => void stepper.goNext()}>Next</button>
  </>
);

API #

PropTypeDefaultDescription
stepsStepperStep[]Required
contentRecord<id, ReactNode>Step content keyed by step id
renderContent(ctx) => ReactNodeAlternative to content
renderStep(ctx) => ReactNodeCustom step indicator
orientation"horizontal" | "vertical""horizontal"Layout direction
size"sm" | "md" | "lg""md"Indicator size
tone"neutral" | "primary""primary"Active accent
mode"linear" | "non-linear""linear"Navigation mode
defaultStep / step / onStepChangecontrolled state0Active step
defaultCompleted / completed / onCompletedChangecontrolled ids[]Completed step ids
onFinish() => voidFires on Finish (after final-step validate)
showFooterbooleantrueBuilt-in Back / Next / Finish buttons
clickableStepsbooleantrueAllow clicking visited / earlier steps
progressBarbooleanfalseRender a thin progress bar above the steps
labels{ back?, next?, finish?, optional? }Footer button + "(optional)" labels

StepperStep #

FieldTypeDescription
idstringRequired, unique
labelReactNodeRequired
descriptionReactNode?Sub-text below the label
iconReactNode?Custom indicator icon
disabledboolean?Skipped by prev/next
optionalboolean?Renders "(optional)" next to the label
errorboolean?Mark step as failed (shake animation, danger ring)
validate() => boolean | string | Promise<...>Run before leaving this step
Edit this page on GitHub