npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@d1os/simple-wizard

v1.2.0

Published

A simple wizard component for React

Readme

simple-wizard

A lightweight, flexible wizard component for React 18 and 19.

Installation

npm install @d1os/simple-wizard
yarn add @d1os/simple-wizard
pnpm add @d1os/simple-wizard

Quick Start

import { Wizard, useWizard } from "@d1os/simple-wizard";

function App() {
  return (
    <Wizard footer={<WizardFooter />}>
      <StepOne />
      <StepTwo />
      <StepThree />
    </Wizard>
  );
}

function WizardFooter() {
  const { nextStep, previousStep, activeStep, stepCount } = useWizard((state) => ({
    nextStep: state.nextStep,
    previousStep: state.previousStep,
    activeStep: state.activeStep,
    stepCount: state.stepCount,
  }));

  const isFirstStep = activeStep === 0;
  const isLastStep = activeStep === stepCount - 1;

  return (
    <div>
      <button onClick={() => previousStep()} disabled={isFirstStep}>
        Back
      </button>
      <button onClick={() => nextStep()} disabled={isLastStep}>
        {isLastStep ? "Finish" : "Next"}
      </button>
    </div>
  );
}

API

<Wizard />

The main container component.

interface WizardProps {
  children?: ReactNode;    // Step components
  header?: ReactNode;      // Rendered before steps
  footer?: ReactNode;      // Rendered after steps
  startAtEnd?: boolean;    // Start at last step (default: false)
  initialStep?: number;    // Starting step index (default: 0)
}

useWizard(selector)

Hook to access wizard state and actions. Uses selectors for optimal re-renders.

const { activeStep, nextStep } = useWizard((state) => ({
  activeStep: state.activeStep,
  nextStep: state.nextStep,
}));

State

| Property | Type | Description | |----------|------|-------------| | activeStep | number | Current step index (0-based) | | stepCount | number | Total number of steps | | isLoading | boolean | Loading state during async actions | | nextButtonLabel | string | Label for next button (default: "Next") | | previousButtonLabel | string | Label for previous button (default: "Back") | | isNextButtonDisabled | boolean | Whether next button is disabled |

Actions

| Action | Type | Description | |--------|------|-------------| | nextStep(skip?) | (skip?: number) => void | Go to next step (or skip multiple) | | previousStep(skip?) | (skip?: number) => void | Go to previous step (or skip multiple) | | setActiveStep(step) | (step: number) => void | Jump to specific step | | setIsLoading(loading) | (loading: boolean) => void | Set loading state | | setStepAction(handler) | (handler: () => void \| Promise<void>) => void | Set action to run before advancing | | setNextButtonLabel(label) | (label: string) => void | Set next button label | | setPreviousButtonLabel(label) | (label: string) => void | Set previous button label | | setNextButtonDisabled(disabled) | (disabled: boolean) => void | Disable/enable next button |

Examples

Async Step Validation

Run async validation before advancing to the next step:

function StepWithValidation() {
  const { setStepAction } = useWizard((state) => ({
    setStepAction: state.setStepAction,
  }));

  useEffect(() => {
    setStepAction(async () => {
      await saveFormData();
      // If this throws, navigation is cancelled
    });
  }, [setStepAction]);

  return <form>...</form>;
}

Skip Steps Conditionally

Use the skip prop to conditionally skip steps:

<Wizard>
  <StepOne />
  <StepTwo skip={!showOptionalStep} />
  <StepThree />
</Wizard>

Custom Progress Indicator

function ProgressBar() {
  const { activeStep, stepCount } = useWizard((state) => ({
    activeStep: state.activeStep,
    stepCount: state.stepCount,
  }));

  const progress = ((activeStep + 1) / stepCount) * 100;

  return (
    <div className="progress-bar">
      <div className="progress-fill" style={{ width: `${progress}%` }} />
      <span>{activeStep + 1} of {stepCount}</span>
    </div>
  );
}

// Usage
<Wizard header={<ProgressBar />}>
  ...
</Wizard>

Controlled Navigation

function StepButtons() {
  const { activeStep, stepCount, setActiveStep } = useWizard((state) => ({
    activeStep: state.activeStep,
    stepCount: state.stepCount,
    setActiveStep: state.setActiveStep,
  }));

  return (
    <div className="step-buttons">
      {Array.from({ length: stepCount }, (_, i) => (
        <button
          key={i}
          onClick={() => setActiveStep(i)}
          className={activeStep === i ? "active" : ""}
        >
          {i + 1}
        </button>
      ))}
    </div>
  );
}

Using with Form Libraries

simple-wizard is form-library agnostic. You can use it with any form library you like, such as react-hook-form, formik, @tanstack/react-form, etc.

Use setStepAction() to integrate validation:

Using react-hook-form

import { useForm } from "react-hook-form";

function StepWithValidation() {
  const { register, trigger } = useForm();
  const { setStepAction } = useWizard((state) => ({
    setStepAction: state.setStepAction,
  }));


  useEffect(() => {
    setStepAction(async () => {
      const valid = await trigger();
      if (!valid) {
        throw new Error("Form is invalid");
      }
    });
  }, [setStepAction, trigger]);

  return (
    <input {...register("email", { required: true })} placeholder="Email" />
  );
}

Using @tanstack/react-form

import { useForm } from "@tanstack/react-form";

function StepWithValidation() {
  const form = useForm({
    defaultValues: {
      email: "",
    },
  });
  const { setStepAction } = useWizard((state) => ({
    setStepAction: state.setStepAction,
  }));

  useEffect(() => {
    setStepAction(async () => {
      await form.validateAllFields("change");
      if (!form.state.isFormValid) {
        throw new Error("Form is invalid");
      }
    });
  }, [setStepAction, form]);

  return (
    <form.Field name="email">
      {({ field }) => (
        <input {...field.getInputProps()} placeholder="Email" />
      )}
    </form.Field>
  )
}

A more advanced example using @tanstack/react-form can be found in this stackblitz.

React 19 Best Practices

This library is fully compatible with React 19. Here are patterns to leverage React 19 features in your step components:

Using use() for Data Fetching

import { use, Suspense } from "react";

function StepWithData({ dataPromise }) {
  const data = use(dataPromise);
  return <div>{data.content}</div>;
}

// In your wizard
<Wizard>
  <Suspense fallback={<Loading />}>
    <StepWithData dataPromise={fetchStepData()} />
  </Suspense>
</Wizard>

Optimistic Updates with useOptimistic()

import { useOptimistic, useState } from "react";

function StepWithOptimisticSave() {
  const [saved, setSaved] = useState(false);
  const [optimisticSaved, setOptimisticSaved] = useOptimistic(saved);

  async function handleSave() {
    setOptimisticSaved(true);  // Instant UI feedback
    await saveToServer();
    setSaved(true);
  }

  return (
    <div>
      <button onClick={handleSave}>
        {optimisticSaved ? "Saved!" : "Save"}
      </button>
    </div>
  );
}

Form Actions with useActionState()

import { useActionState } from "react";

function FormStep() {
  const [state, submitAction, isPending] = useActionState(
    async (prevState, formData) => {
      const result = await submitForm(formData);
      return { success: result.ok, error: result.error };
    },
    { success: false, error: null }
  );

  return (
    <form action={submitAction}>
      <input name="email" type="email" required />
      {state.error && <p className="error">{state.error}</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? "Submitting..." : "Continue"}
      </button>
    </form>
  );
}

TypeScript

Full TypeScript support with exported types:

import type {
  WizardProps,
  WizardStore,
  WizardState,
  WizardActions
} from "@d1os/simple-wizard";

License

MIT