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

react-formsteps-core

v0.1.2

Published

Headless multi-step form logic for React

Readme

react-formsteps

Headless, type-safe multi-step form library for React — built on react-hook-form and Zod.

npm version npm version license TypeScript docs


Packages

| Package | Version | Description | | ------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------- | | react-formsteps-core | npm | Headless hooks + context. No UI, no styles. | | react-formsteps-ui | npm | Optional pre-built React components. |


Features

  • Headless — zero UI imposed. Works with any design system.
  • Per-step validation — validates only the current step's Zod schema before advancing.
  • Type-safe — strict TypeScript throughout. Types flow from Zod schemas into your fields.
  • Built on react-hook-form — full compatibility with the RHF ecosystem.
  • Flexible — use the hooks alone or drop in the ready-made components.
  • Tiny — tree-shakeable, no CSS bundled.

Installation

Core only (headless)

# npm
npm install react-formsteps-core react-hook-form zod @hookform/resolvers

# pnpm
pnpm add react-formsteps-core react-hook-form zod @hookform/resolvers

# yarn
yarn add react-formsteps-core react-hook-form zod @hookform/resolvers

With UI components

# npm
npm install react-formsteps-core react-formsteps-ui react-hook-form zod @hookform/resolvers

# pnpm
pnpm add react-formsteps-core react-formsteps-ui react-hook-form zod @hookform/resolvers

Peer dependencies

| Dependency | Version | | --------------------- | ------- | | react | >=18 | | react-dom | >=18 | | react-hook-form | >=7 | | zod | >=3 | | @hookform/resolvers | >=3 |


Quick start

Headless — useSteps + useStepForm

import { useSteps, useStepForm } from 'react-formsteps-core';
import { z } from 'zod';

const schemas = [
  z.object({ firstName: z.string().min(1, 'Required'), lastName: z.string().min(1, 'Required') }),
  z.object({ email: z.string().email('Enter a valid email') }),
  z.object({ password: z.string().min(8, 'Min. 8 characters') }),
];

export function RegistrationForm() {
  const { currentStep, next, prev, isFirst, isLast, progress, totalSteps } = useSteps({
    totalSteps: schemas.length,
  });

  const { form, nextWithValidation, isValidating } = useStepForm({
    schema: schemas[currentStep],
    onNext: (data) => console.log('Step data:', data),
  });

  const handleNext = async () => {
    const ok = await nextWithValidation();
    if (ok && !isLast) next();
    if (ok && isLast) console.log('All done!', form.getValues());
  };

  return (
    <form>
      {/* Progress */}
      <div>
        Step {currentStep + 1} of {totalSteps} — {progress}%
      </div>
      <progress value={progress} max={100} />

      {/* Step 1 */}
      {currentStep === 0 && (
        <>
          <input {...form.register('firstName')} placeholder="First name" />
          <input {...form.register('lastName')} placeholder="Last name" />
        </>
      )}

      {/* Step 2 */}
      {currentStep === 1 && <input {...form.register('email')} placeholder="Email" />}

      {/* Step 3 */}
      {currentStep === 2 && (
        <input {...form.register('password')} type="password" placeholder="Password" />
      )}

      {/* Navigation */}
      <button type="button" onClick={prev} disabled={isFirst}>
        Back
      </button>
      <button type="button" onClick={handleNext} disabled={isValidating}>
        {isLast ? 'Submit' : 'Next'}
      </button>
    </form>
  );
}

With UI components — <Steps> + <Step>

import { Steps, Step, StepBar, StepNav } from 'react-formsteps-ui';
import { z } from 'zod';

const schema1 = z.object({ name: z.string().min(1, 'Required') });
const schema2 = z.object({ email: z.string().email() });
const schema3 = z.object({ password: z.string().min(8) });

export function RegistrationForm() {
  return (
    <Steps
      schemas={[schema1, schema2, schema3]}
      onSubmit={(data) => console.log('Submitted:', data)}
    >
      <Step title="Personal info">{/* your fields */}</Step>

      <Step title="Contact">{/* your fields */}</Step>

      <Step title="Security">{/* your fields */}</Step>
    </Steps>
  );
}

API

useSteps(options)

Manages step navigation state.

const {
  currentStep,  // number — 0-indexed
  totalSteps,   // number
  isFirst,      // boolean
  isLast,       // boolean
  next,         // () => void
  prev,         // () => void
  goTo,         // (index: number) => void
  progress,     // number — 0 to 100
} = useSteps({ totalSteps: 3, initialStep?: 0, onComplete?: () => void });

| Option | Type | Default | Description | | ------------- | ------------ | ------- | -------------------------------------------------------- | | totalSteps | number | — | Required. Must be a positive integer | | initialStep | number | 0 | Starting step index | | onComplete | () => void | — | Called once when the user first arrives at the last step |


useStepForm(options)

Integrates react-hook-form with per-step Zod validation.

const {
  form,               // UseFormReturn<z.infer<typeof schema>>
  nextWithValidation, // () => Promise<boolean>
  isValidating,       // boolean
} = useStepForm({ schema, defaultValues?, onNext? });

| Option | Type | Description | | --------------- | --------------------------- | --------------------------------------------- | | schema | ZodType | Required. Zod schema for the current step | | defaultValues | Partial<z.infer<TSchema>> | Initial field values | | onNext | (data) => void | Called with validated data when advancing |


useStepsContext()

Access step state from any component inside a <Steps> or <StepsProvider>.

import { useStepsContext } from 'react-formsteps-core';

const { currentStep, totalSteps, next, prev, goTo, formData } = useStepsContext();

<StepsProvider> — headless context

Use the context without the UI package.

import { StepsProvider, useStepsContext } from 'react-formsteps-core';

<StepsProvider schemas={[schema1, schema2]} onSubmit={handleSubmit}>
  <MyCustomWizard />
</StepsProvider>;

Utilities

import { validateStep, mergeSchemas, validateAllSteps } from 'react-formsteps-core';

// Validate a single step — never throws
const result = await validateStep(schema, data);
// { success: boolean, data?, errors?: Record<string, string> }

// Merge multiple ZodObject schemas into one
const fullSchema = mergeSchemas([step1Schema, step2Schema, step3Schema]);

// Validate all accumulated data against merged schemas
const result = await validateAllSteps(schemas, allData);

UI Components

| Component | Description | | ----------- | ----------------------------------------------------------- | | <Steps> | Root provider. Pass schemas and onSubmit. | | <Step> | Wrapper for each step's content. Accepts optional title. | | <StepBar> | Progress bar with optional step labels. | | <StepNav> | Back / Next / Submit buttons with built-in validation gate. |


TypeScript

All APIs are fully typed. Enable strict: true in your tsconfig.json for the best experience.

{
  "compilerOptions": {
    "strict": true
  }
}

Import types directly:

import type {
  StepSchema,
  StepConfig,
  StepsContextValue,
  UseStepsOptions,
  UseStepsReturn,
  UseStepFormOptions,
  UseStepFormReturn,
  StepsProviderProps,
} from 'react-formsteps-core';

Contributing

Contributions via pull requests are welcome. Please open an issue first to discuss significant changes.

By submitting a contribution you agree that the original author (Franxx) retains full ownership and copyright of the project, including your contributions.


License

Copyright (c) 2025 Franxx — see LICENSE for full terms.

This software is free for personal, educational, and open source use. Commercial use requires explicit written permission from the author. See the license for details.