A flexible, headless, and strictly typed multi-step wizard library for React. Built with adapter patterns in mind to support any form library (React Hook Form, Formik, etc.) and any validation schema (Zod, Yup).
- π§ Headless Architecture: Full control over UI. You bring the components, we provide the logic.
- π Adapter Pattern: Zero-dependency adapters for Zod, Yup validation.
- ποΈ Complex Data: Built-in support for nested objects and arrays using dot notation (
users[0].name). - π‘οΈ Strictly Typed: Built with TypeScript generics for type safety across steps.
- πΎ Advanced Persistence: Auto-save progress to LocalStorage, custom stores, or Hybrid Step-Level persistence.
- οΏ½ Comprehensive Guides: Detailed documentation portal with interactive guides, pro-tips, and type references.
- β‘ High Performance Engine: Path caching, Hash-Map lookups, and Stateless Provider architecture.
npm install wizzard-stepper-react
# or
yarn add wizzard-stepper-react
# or
pnpm add wizzard-stepper-reactThe quickest way to get started. Types are flexible (any).
import { WizardProvider, useWizard } from 'wizzard-stepper-react';
const Step1 = () => {
const { wizardData, handleStepChange } = useWizard();
return (
<input
value={wizardData.name}
onChange={(e) => handleStepChange('name', e.target.value)}
/>
);
};
const App = () => (
<WizardProvider>
<Step1 />
</WizardProvider>
);For production apps, use the factory pattern to get perfect type inference.
wizards/my-wizard.ts
import { createWizardFactory } from 'wizzard-stepper-react';
interface MySchema {
name: string;
age: number;
}
export const { WizardProvider, useWizard } = createWizardFactory<MySchema>();components/MyForm.tsx
import { useWizard } from '../wizards/my-wizard';
const Step1 = () => {
const { wizardData } = useWizard();
// β
wizardData is strictly typed as MySchema
// β
Autocomplete works for wizardData.name
}See MIGRATION.md for details on switching to strict mode.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { ZodAdapter, useWizard } from 'wizzard-stepper-react';
const schema = z.object({ email: z.string().email() });
const MyStep = () => {
const { handleStepChange, wizardData } = useWizard();
const { register } = useForm({
defaultValues: wizardData,
resolver: zodResolver(schema),
mode: 'onChange' // Important: validate real-time or bind changes
});
return (
<input {...register('email', {
onChange: (e) => handleStepChange('email', e.target.value)
})} />
);
}
// In Config:
const config = {
steps: [
{
id: 'step1',
label: 'Email',
// Zero-dependency: works with any Zod version
validationAdapter: new ZodAdapter(schema)
}
]
}The library provides setData and getData helpers that support deep paths using dot notation and array brackets.
const { setData, wizardData } = useWizard<MyData>();
// Set nested object property
setData('user.profile.name', 'John');
// Set array item property
setData('items[0].value', 'New Value');
// Get with default value
const name = getData('user.profile.name', 'Anonymous');
// π Bulk Update (Autofill)
const autoFillParams = () => {
// Merges into existing data
updateData({
name: 'John Doe',
email: 'john@example.com'
});
};New in v2: The library now uses path caching and iterative updates under the hood. setData is incredibly fast even for deeply nested objects.
For large forms (e.g., 50+ array items), using useWizard context can still cause React re-renders. To solve this, we provide granular hooks:
Instead of reading the whole wizardData, subscribe to a single field. The component will only re-render when that specific field changes.
// β
FAST: Only re-renders when "users[0].name" changes
const NameInput = () => {
// Subscribe to specific path
const name = useWizardValue('users[0].name');
const { setData } = useWizardActions(); // Component actions don't trigger re-renders
return <input value={name} onChange={e => setData('users[0].name', e.target.value)} />;
}
// β SLOW: Re-renders on ANY change in the form
const NameInputSlow = () => {
const { wizardData, setData } = useWizard();
return <input value={wizardData.users[0].name} ... />;
}When rendering lists, avoid passing the whole children array to the parent component. Instead, select only IDs and let child components fetch their own data.
const ChildrenList = () => {
// β
Only re-renders when the list LENGTH changes or IDs change
const childIds = useWizardSelector(state => state.children.map(c => c.id));
return (
<div>
{childIds.map((id, index) => (
// Pass ID/Index, NOT the data object
<ChildRow key={id} index={index} />
))}
</div>
);
}For heavy validation schemas, you can debounce validation to keep the UI responsive.
setData('field.path', value, {
debounceValidation: 300 // Wait 300ms before running Zod/Yup validation
});By default, validation runs on every change (onChange). You can optimize this for heavy forms.
-
π§© Flexible adapters for any validation or persistence logic
-
β‘ High Performance: Stateless Provider architecture and O(1) internal lookups
-
π¦ Zero Dependencies (excluding React as peer) You can control when validation happens using
validationMode. This is critical for performance in large forms or heavy validation schemas. -
onChange(Default): Validates fields as you type (debounced). Best for immediate feedback. -
onStepChange: Validates ONLY when trying to move to the next step.- Ux improvement: If an error occurs, it appears. But as soon as the user starts typing to fix it, the error is immediately cleared locally (without triggering a full re-validation schema check), providing instant "clean" feedback.
-
manual: No automatic validation. You manually callvalidateStep().
const config: IWizardConfig = {
steps: [
{
id: 'heavy-step',
label: 'Complex Data',
// Optimize: Only validate on "Next" click
validationMode: 'onStepChange',
validationAdapter: new ZodAdapter(heavySchema)
}
]
};- Hash Table Storage: Errors are stored internally using
Map(Hash Table) for O(1) access and deletion, ensuring UI stays snappy even with hundreds of errors. - Path Caching: Data access paths (e.g.,
users[0].name) are cached to eliminate parsing overhead during frequent typing.
Steps can be dynamically included based on the wizard's state.
const config: IWizardConfig = {
steps: [
{ id: 'start', label: 'Start' },
{
id: 'bonus',
label: 'Bonus Step',
// Only show if 'wantBonus' is true
condition: (data) => !!data.wantBonus
}
]
}Automatically save wizard progress so users don't lose data on reload.
import { LocalStorageAdapter } from 'wizzard-stepper-react';
const config: IWizardConfig = {
steps: [...],
persistence: {
adapter: new LocalStorageAdapter('my-wizard-key'),
mode: 'onStepChange' // or 'onChange' (heavier)
}
};Everything (current step, data, visited state) is handled automatically! LocalStorage to survive page reloads.
Instead of manual switch statements with currentStep.id, trust the renderer!
// Define component in config
const steps = [
{ id: 'step1', label: 'Start', component: Step1Component },
{ id: 'step2', label: 'End', component: Step2Component },
];
// Render
const App = () => (
<WizardProvider config={{ steps }}>
<WizardStepRenderer
wrapper={({ children }) => (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{children}
</motion.div>
)}
/>
</WizardProvider>
);Sync wizard state with URL using onStepChange.
const navigate = useNavigate();
const config: IWizardConfig = {
// 1. Sync State -> URL
onStepChange: (prev, next, data) => {
navigate(`/wizard/${next}`);
// Optional: Send event to Analytics
trackEvent('wizard_step', { step: next });
},
steps: [...]
};
// 2. Sync URL -> State (Initial Load)
const { stepId } = useParams();
return <WizardProvider config={config} initialStepId={stepId}>...</WizardProvider>;By default, the wizard uses MemoryAdapter (RAM only). You can enable LocalStorage globally, but override it or its behavior (mode) for individual steps.
const config: IWizardConfig = {
// Global: Persist everything to LocalStorage on every step change
persistence: {
adapter: new LocalStorageAdapter('wizard_'),
mode: 'onStepChange'
},
steps: [
{
id: 'public',
label: 'Public Info',
// Inherits global settings
},
{
id: 'realtime',
label: 'Active Draft',
// Override Mode: Save to local storage on every keystroke
persistenceMode: 'onChange'
},
{
id: 'sensitive',
label: 'Credit Card',
// Override Adapter: Store strictly in memory (cleared on refresh)
persistenceAdapter: new MemoryAdapter()
}
]
};steps: Array of step configurations.persistence: Configuration for state persistence.autoValidate: (obj) Global validation setting.
activeSteps: Steps that match conditions.currentStep: The currently active step object.wizardData: The global state object (subscribe cautiously!).setData(path, value, options?): Update state. Options:{ debounceValidation: number }.getData(path, defaultValue?): Retrieve nested data.handleStepChange(key, value): simple helper to update top-level state.goToNextStep(): Validates and moves next.goToStep(id): Jumps to specific step.allErrors: Map of validation errors.
Subscribes to a specific data path. Re-renders only when that value changes.
Subscribes to validation errors for a specific path. Highly recommended for individual inputs.
Create a custom subscription to the wizard state. Ideal for derived state or lists.
Returns object with actions (setData, goToNextStep, etc.) without subscribing to state changes. Use this in components that trigger updates but don't need to render data.
Check out the Live Demo, NPM or the source code for a complete implementation featuring:
- Premium Documentation: Interactive Guides and a dedicated Type Reference.
- Tailwind CSS v4 UI overhaul.
- React Hook Form + Zod integration.
- Formik + Yup integration.
- Conditional Routing logic.
- Advanced Features Demo: (
/advanced) showcasing:- Autofill:
updateDataglobal merge. - Declarative Rendering:
<WizardStepRenderer />. - Granular Persistence: Hybrid Memory/LocalStorage.
- Autofill:
Visit /advanced in the demo to try:
- Autofill: Click "Magic Autofill" to test
updateData(). It instantly populates the form (merged with existing data). - Hybrid Persistence:
- Step 1 (Identity): Refreshes persist (LocalStorage).
- Step 2 (Security): Refreshes CLEAR data (MemoryAdapter).
- Declarative UI: The steps are rendered using
<WizardStepRenderer />with Framer Motion animations, defined in the config!
wizzard-stepper-react is architected for extreme scale.
- Stateless Provider: The main provider doesn't re-render when field values change. Only the specific components subscribed to those fields (via
useWizardValue) re-render. - O(1) Engine: Internal lookups for step configuration, active steps, and indices use Hash Maps to ensure instant response times even with hundreds of steps.
- Deeply Nested Optimization: Data access uses a memoized iterative path traversal, avoiding recursive overhead in deeply nested objects.
- Loading State: Built-in
isLoadingstate to handle high-latency persistence restoration gracefully.
const { isLoading } = useWizardState();
if (isLoading) return <SkeletonLoader />;MIT