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.

📄 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} />
      )}
    />
  )
}
Home Experience Projects