@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 #
| Prop | Type | Default | Description |
|---|---|---|---|
| steps | StepperStep[] | — | Required |
| content | Record<id, ReactNode> | — | Step content keyed by step id |
| renderContent | (ctx) => ReactNode | — | Alternative to content |
| renderStep | (ctx) => ReactNode | — | Custom 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 / onStepChange | controlled state | 0 | Active step |
| defaultCompleted / completed / onCompletedChange | controlled ids | [] | Completed step ids |
| onFinish | () => void | — | Fires on Finish (after final-step validate) |
| showFooter | boolean | true | Built-in Back / Next / Finish buttons |
| clickableSteps | boolean | true | Allow clicking visited / earlier steps |
| progressBar | boolean | false | Render a thin progress bar above the steps |
| labels | { back?, next?, finish?, optional? } | — | Footer button + "(optional)" labels |
StepperStep #
| Field | Type | Description |
|---|---|---|
| id | string | Required, unique |
| label | ReactNode | Required |
| description | ReactNode? | Sub-text below the label |
| icon | ReactNode? | Custom indicator icon |
| disabled | boolean? | Skipped by prev/next |
| optional | boolean? | Renders "(optional)" next to the label |
| error | boolean? | Mark step as failed (shake animation, danger ring) |
| validate | () => boolean | string | Promise<...> | Run before leaving this step |