Open Source
stepper-ui
A React component library for building accessible, customizable multi-step forms with TypeScript support.
Live Demo
Fill out the form to see validation in action
Step 1
Step 2
Installation
Install the package
npm install stepper-ui
Install peer dependencies
npm install react react-dom clsx tailwind-merge
Quick Start
Basic implementation with two steps and validation
your-stepper.tsx
import { Stepper } from 'stepper-ui'
import { FormPersonalData } from './FormPersonalData'
import { FormAccount } from './FormAccount'
const MyForm = () => {
const steps = [
{ name: 'Personal', component: FormPersonalData },
{ name: 'Account', component: FormAccount }
]
return (
<Stepper
steps={steps}
renderButtons={({ backStep, nextStep }) => (
<div className="flex justify-between">
<button onClick={backStep}>Previous</button>
<button onClick={nextStep}>Next</button>
</div>
)}
/>
)
} Project Structure
src/
components/
stepper/
├── StepIcon.tsx
├── StepperButtons.tsx
├── StepOne.tsx
└── StepTwo.tsx
stepper-ui.tsx
Full Example
stepper-ui.tsx
Main wrapper component stepper-ui.tsx
import { Stepper } from 'stepper-ui'
import { FirstForm } from './stepper/first-form'
import { SecondForm } from './stepper/second-form'
const StepIcon = ({
label,
isActive,
isCompleted
}: {
label: string
step: number
isActive: boolean
isCompleted: boolean
}) => {
return (
<div
className={`px-8 py-2 text-sm font-medium rounded-full border flex items-center justify-center transition-all duration-300
${isActive ? 'border-zinc-400 bg-zinc-800 text-zinc-100 shadow-lg shadow-zinc-500/10' : 'border-zinc-700 text-zinc-500'}
${isCompleted ? 'bg-zinc-100 border-zinc-100 text-zinc-900' : ''}`}
>
{isCompleted ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : null}
{label && <span className={isCompleted ? 'ml-2' : 'ml-2'}>{label}</span>}
</div>
)
}
const StepperButtons = ({
backStep,
nextStep
}: {
backStep: () => void
nextStep: () => void
}) => {
return (
<div className='flex justify-between mt-8 pt-6 border-t border-zinc-800'>
<button
onClick={backStep}
type='button'
className='px-5 py-2.5 text-sm font-medium text-zinc-400 border border-zinc-700 rounded-lg hover:bg-zinc-800 hover:text-zinc-200 hover:border-zinc-600 transition-all'
>
Previous
</button>
<button
onClick={nextStep}
className='px-5 py-2.5 text-sm font-medium bg-zinc-100 text-zinc-900 rounded-lg hover:bg-zinc-200 transition-all'
>
Next
</button>
</div>
)
}
const steps = [
{ name: 'Step 1', component: FirstForm },
{ name: 'Step 2', component: SecondForm }
]
export const StepperUI = () => {
return (
<Stepper
wrapperClassName='w-full mx-auto'
renderStepIcon={(label, step, isActive, isCompleted) => (
<StepIcon
label={label}
step={step}
isActive={isActive}
isCompleted={isCompleted}
/>
)}
steps={steps}
renderButtons={({ backStep, nextStep }) => (
<StepperButtons backStep={backStep} nextStep={nextStep} />
)}
/>
)
}
first-form.tsx
Step 1: Personal information with validation first-form.tsx
import { forwardRef, useImperativeHandle, type ForwardedRef } from 'react'
import type { ValidateStep } from 'stepper-ui'
import { usePersistedState } from '../../../hooks/use-persisted-state'
import { FormField } from './form-field'
import type { FirstFormData, FirstFormErrors } from './types'
export const FirstForm = forwardRef<ValidateStep>(
(_, ref: ForwardedRef<ValidateStep>) => {
const [formData, setFormData] = usePersistedState<FirstFormData>(
'userData',
{
name: '',
email: ''
}
)
const [errors, setErrors] = usePersistedState<FirstFormErrors>(
'errorsStep1',
{}
)
useImperativeHandle(ref, () => ({
canContinue: () => {
const newErrors: FirstFormErrors = {}
if (!formData.name.trim()) newErrors.name = 'Name is required'
if (!formData.email.trim()) newErrors.email = 'Email is required'
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email))
newErrors.email = 'Invalid email format'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
}))
return (
<form className='flex flex-col gap-4'>
<FormField
label='Name'
value={formData.name}
onChange={val => setFormData({ ...formData, name: val })}
placeholder='Enter your name'
error={errors.name}
/>
<FormField
label='Email'
type='email'
value={formData.email}
onChange={val => setFormData({ ...formData, email: val })}
placeholder='Enter your email'
error={errors.email}
/>
</form>
)
}
)
second-form.tsx
Step 2: Password with confirmation second-form.tsx
import { forwardRef, useImperativeHandle, type ForwardedRef } from 'react'
import type { StepperContextProps, ValidateStep } from 'stepper-ui'
import { FormField } from './form-field'
import { usePersistedState } from '../../../hooks/use-persisted-state'
import type { SecondFormData, SecondFormErrors } from './types'
export const SecondForm = forwardRef<ValidateStep, StepperContextProps>(
({ goToInitialStep }, ref: ForwardedRef<ValidateStep>) => {
const [formData, setFormData] = usePersistedState<SecondFormData>(
'userDataPassword',
{ password: '', confirmPassword: '' }
)
const [errors, setErrors] = usePersistedState<SecondFormErrors>(
'errorsStep2',
{}
)
useImperativeHandle(ref, () => ({
canContinue: () => {
const newErrors: SecondFormErrors = {}
if (!formData.password.trim())
newErrors.password = 'Password is required'
else if (formData.password.length < 6)
newErrors.password = 'Must be at least 6 characters'
if (!formData.confirmPassword.trim())
newErrors.confirmPassword = 'Please confirm your password'
else if (formData.password !== formData.confirmPassword)
newErrors.confirmPassword = 'Passwords do not match'
setErrors(newErrors)
if (Object.keys(newErrors).length === 0) {
const userData = JSON.parse(localStorage.getItem('userData') || '{}')
alert(
`Form submitted successfully! ${JSON.stringify({
...userData,
password: formData.password
})}`
)
localStorage.clear()
goToInitialStep()
return true
}
return false
}
}))
return (
<form className='flex flex-col gap-4'>
<FormField
label='Password'
type='password'
value={formData.password}
onChange={val => setFormData({ ...formData, password: val })}
placeholder='Enter a password'
error={errors.password}
/>
<FormField
label='Confirm Password'
type='password'
value={formData.confirmPassword}
onChange={val => setFormData({ ...formData, confirmPassword: val })}
placeholder='Confirm your password'
error={errors.confirmPassword}
/>
</form>
)
}
)
API Reference
| Prop | Type | Description |
|---|---|---|
| steps | StepComponentProps[] | Array of step components |
| renderButtons | RenderButtonsProps | Navigation buttons renderer |
| wrapperClassName | string | Tailwind classes for container |
| renderStepIcon | function | Custom step indicator renderer |
Features
TypeScript Support
Full type definitions included. Step components can use forwardRef with ValidateStep for validation.
Validation
Sync and async validation. canContinue can return boolean or Promise<boolean>.
Customizable
Custom step icons, classes, and rendering for complete flexibility.
Performant
Memoized components prevent unnecessary re-renders. Only active step is mounted.
Ready to use?
Install stepper-ui and start building multi-step forms today.
npm install stepper-ui