react-hook-form-stepper-formik
v1.0.0
Published
A production-ready multi-step form manager for React applications with support for React Hook Form and Formik.
Maintainers
Readme
react-smart-stepper
A production-ready multi-step form manager for React applications. Supports step-level validation (sync & async), form persistence, and first-class integrations with React Hook Form and Formik.
✨ Features
- 🧭 Step Navigation —
next,previous,goToStepwith skip-step logic - ✅ Validation — sync and async validators per step
- 💾 Persistence — optional
localStoragestate persistence - 📊 Progress Tracking — bar, dots, and numbered step indicators
- 🔗 Integrations — React Hook Form, Formik, and native React state
- 🔒 Fully Typed — generic
StepperForm<TData>for complete type safety - 🌳 Tree-shakable — ESM + CJS dual build via
tsup - ♿ Accessible — ARIA attributes on all interactive elements
📦 Installation
npm install react-hook-form-stepper-formik
# or
yarn add react-hook-form-stepper-formik
# or
pnpm add react-hook-form-stepper-formikPeer dependencies:
react >= 18andreact-dom >= 18are required.
🚀 Quick Start
import {
StepperForm,
Step,
StepperProgress,
useStepper,
} from 'react-hook-form-stepper-formik';
interface FormData {
name: string;
email: string;
plan: string;
}
const steps = [
{ id: 'info', title: 'Info', validate: (d: FormData) => !!d.name || 'Name is required' },
{ id: 'contact', title: 'Contact', validate: (d: FormData) => d.email.includes('@') || 'Invalid email' },
{ id: 'plan', title: 'Plan' },
];
function Navigation() {
const { currentStep, totalSteps, nextStep, prevStep, isFirstStep, isLastStep, isValidating } = useStepper();
return (
<div>
<span>Step {currentStep + 1} of {totalSteps}</span>
<button onClick={prevStep} disabled={isFirstStep}>Back</button>
<button onClick={nextStep} disabled={isValidating}>
{isLastStep ? 'Submit' : 'Next'}
</button>
</div>
);
}
export default function MyForm() {
return (
<StepperForm<FormData>
steps={steps}
initialData={{ name: '', email: '', plan: '' }}
onComplete={(data) => console.log('Done!', data)}
persist
persistKey="my-form"
>
<StepperProgress variant="steps" showTitles />
<Step index={0}><NameStep /></Step>
<Step index={1}><EmailStep /></Step>
<Step index={2}><PlanStep /></Step>
<Navigation />
</StepperForm>
);
}📖 API Reference
<StepperForm>
The root provider. Wrap all steps inside it.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| steps | StepConfig<TData>[] | required | Array of step configuration objects |
| initialData | TData | {} | Initial form data |
| onComplete | (data: TData) => void \| Promise<void> | — | Called when last step is submitted |
| onStepChange | (step: number, data: TData) => void | — | Called on every step navigation |
| persist | boolean | false | Enable localStorage persistence |
| persistKey | string | — | Unique key for localStorage (required if persist=true) |
| children | ReactNode \| (ctx) => ReactNode | — | Children or render prop |
| className | string | — | CSS class for the wrapper div |
StepConfig<TData>
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique step identifier |
| title | string | Display title (used in progress indicator) |
| description | string | Optional description |
| validate | (data: TData) => boolean \| string \| Promise<boolean \| string> | Validation function. Return true to pass, or an error string to fail. |
| skip | (data: TData) => boolean | Conditionally skip this step |
| icon | ReactNode | Custom icon for the step indicator |
useStepper<TData>()
Access all stepper state and actions from any child component.
const {
currentStep, // number — current step index
totalSteps, // number
isFirstStep, // boolean
isLastStep, // boolean
progress, // number — 0–100
isValidating, // boolean — true during async validation
formData, // TData
errors, // Record<string, string>
stepStatuses, // StepStatus[]
visitedSteps, // Set<number>
steps, // StepConfig<TData>[]
nextStep, // () => Promise<void>
prevStep, // () => void
goToStep, // (index: number) => void
setFormData, // (data: Partial<TData>) => void
setErrors, // (errors: Record<string, string>) => void
resetStepper, // () => void
} = useStepper<TData>();<Step>
Renders children only when its index matches the active step.
| Prop | Type | Description |
|------|------|-------------|
| index | number | The step index this content belongs to |
| children | ReactNode | Step content |
| className | string | Optional CSS class |
<StepperProgress>
Visual progress indicator.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'bar' \| 'dots' \| 'steps' \| 'numbers' | 'steps' | Visual style |
| showTitles | boolean | true | Show step titles below indicators |
| showNumbers | boolean | true | Show step numbers in circles |
| clickable | boolean | false | Allow clicking indicators to navigate |
| renderStep | (step, index, status, isCurrent) => ReactNode | — | Custom step renderer |
| className | string | — | CSS class for the wrapper |
🔗 Integration — React Hook Form
Use useForm inside each step, and sync values to the stepper on onBlur:
import { useForm } from 'react-hook-form';
import { useStepper } from 'react-smart-stepper';
function EmailStep() {
const { setFormData, formData } = useStepper<{ email: string }>();
const { register, formState: { errors }, trigger, getValues } = useForm({
defaultValues: { email: formData.email },
});
return (
<form>
<input
{...register('email', { required: true, pattern: /\S+@\S+/ })}
onBlur={async () => {
await trigger('email');
setFormData(getValues());
}}
/>
{errors.email && <span>Invalid email</span>}
</form>
);
}Use the step-level validate to guard navigation:
const steps = [
{
id: 'email',
validate: (d) => d.email.includes('@') || 'Please enter a valid email',
},
];🧬 Integration — Formik + Yup
Use Formik inside each step and Yup schemas for the step-level validate:
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import { useStepper } from 'react-smart-stepper';
const schema = Yup.object({ name: Yup.string().required() });
function NameStep() {
const { formData, setFormData } = useStepper<{ name: string }>();
return (
<Formik initialValues={{ name: formData.name }} validationSchema={schema} onSubmit={() => {}}>
{({ getFieldProps, handleBlur }) => (
<Form>
<Field
name="name"
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
handleBlur(e);
setFormData({ name: e.target.value });
}}
/>
</Form>
)}
</Formik>
);
}
// Step validates via Yup
const steps = [{
id: 'name',
validate: async (d) => {
try { await schema.validate(d); return true; }
catch (e) { return (e as Error).message; }
},
}];🔧 Advanced Examples
Async Validation
const steps = [{
id: 'username',
validate: async (data) => {
const taken = await checkUsernameAvailability(data.username);
return taken ? 'Username is already taken' : true;
},
}];Conditional Step Skipping
const steps = [
{ id: 'type', title: 'Type' },
{
id: 'company',
title: 'Company',
skip: (data) => data.accountType !== 'business', // skipped for personal accounts
},
{ id: 'confirm', title: 'Confirm' },
];Programmatic Navigation
function JumpButton() {
const { goToStep, resetStepper } = useStepper();
return (
<>
<button onClick={() => goToStep(2)}>Jump to Step 3</button>
<button onClick={resetStepper}>Start Over</button>
</>
);
}Render Prop Pattern
<StepperForm steps={steps} initialData={{}}>
{({ currentStep, progress, formData }) => (
<div>
<p>Progress: {progress}%</p>
<Step index={currentStep}>...</Step>
</div>
)}
</StepperForm>Custom Progress Indicator
<StepperProgress
variant="steps"
clickable
renderStep={(step, index, status, isCurrent) => (
<div style={{ color: isCurrent ? 'purple' : 'gray' }}>
{status === 'completed' ? '✓' : index + 1}
</div>
)}
/>🏗 Project Structure
react-smart-stepper/
├── src/
│ ├── components/
│ │ ├── StepperForm.tsx # Root provider
│ │ ├── Step.tsx # Step content wrapper
│ │ └── StepperProgress.tsx # Progress indicator
│ ├── hooks/
│ │ └── useStepper.ts # State access hook
│ ├── context/
│ │ ├── StepperContext.tsx # React context
│ │ └── stepperReducer.ts # Pure state reducer
│ ├── types/
│ │ └── stepper.types.ts # All TypeScript types
│ ├── utils/
│ │ └── persistence.ts # localStorage helpers
│ └── index.ts # Public barrel export
├── example/
│ └── demo-app/ # Vite demo app
├── tsup.config.ts
├── tsconfig.json
└── package.json🏭 Building
# Build the library
npm run build
# Type-check only
npm run type-check
# Watch mode
npm run build:watch🖥 Running the Demo
cd example/demo-app
npm install
npm run dev📄 License
MIT © Your Name
