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