Stepper UI Demo
Step 1
Step 2
Documentation & Usage
StepperUI is a React multi-step form component with validation and state persistence. Navigate through steps, fill forms, and see real-time updates.
- Supports multiple steps
- Customizable step icons and buttons
📄 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
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>
)
}
)
📄 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-10 py-2 text-white rounded-full border border-purple-600 flex items-center justify-center transition-all duration-300
${isActive ? 'shadow-purple-500/30 shadow-xl' : ''}
${isCompleted ? 'bg-purple-600' : ''}`}
>
{isCompleted && '✓'}
{label && <span className='ml-2'>{label}</span>}
</div>
)
}
const StepperButtons = ({
backStep,
nextStep
}: {
backStep: () => void
nextStep: () => void
}) => {
return (
<div className='flex justify-between mt-5'>
<button
onClick={backStep}
type='button'
className='bg-purple-500/15 border border-purple-600 rounded-xl hover:border-purple-500 text-white px-4 py-2 transition'
>
Previous
</button>
<button
onClick={nextStep}
className='bg-purple-500/15 border border-purple-600 rounded-xl hover:border-purple-500 text-white px-4 py-2 transition'
>
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 bg-white/5 rounded-lg shadow-lg p-5 shadow-md shadow-purple-500/20 text-white'
renderStepIcon={(label, step, isActive, isCompleted) => (
<StepIcon
label={label}
step={step}
isActive={isActive}
isCompleted={isCompleted}
/>
)}
steps={steps}
renderButtons={({ backStep, nextStep }) => (
<StepperButtons backStep={backStep} nextStep={nextStep} />
)}
/>
)
}