Skip to main content
Use Case

Build a Multi-Step Wizard Form with React and Tailwind CSS v4

Build an accessible multi-step form wizard with step navigation, validation per step, and a progress indicator — using Ninna UI components and Tailwind v4.

Wizard form structure

A multi-step wizard breaks a long form into smaller steps. Each step has its own validation, and users can navigate forward/backward. The key accessibility concern is focus management: move focus to the step heading when the step changes.

1. Step indicator

Use a horizontal stepper with numbered circles:

import { HStack } from "@ninna-ui/layout";
import { Text } from "@ninna-ui/primitives";

<HStack gap="2" className="mb-8">
  {steps.map((step, i) => (
    <div key={i} className="flex items-center gap-2">
      <div className={`flex size-8 items-center justify-center rounded-full text-sm font-medium ${i === currentStep ? "bg-primary text-primary-content" : i < currentStep ? "bg-success text-success-content" : "bg-base-200 text-base-content/60"}`}>
        {i < currentStep ? "✓" : i + 1}
      </div>
      <Text size="sm" className={i === currentStep ? "font-medium" : "text-base-content/60"}>{step.title}</Text>
      {i < steps.length - 1 && <div className="w-8 h-px bg-base-300" />}
    </div>
  ))}
</HStack>

2. Step content with validation

Render the current step's form fields. Validate before allowing the user to proceed:

import { Field, Input } from "@ninna-ui/forms";
import { Button } from "@ninna-ui/primitives";
import { VStack, HStack } from "@ninna-ui/layout";

<VStack gap="4">
  <Heading as="h2" size="xl" ref={stepHeadingRef}>{steps[currentStep].title}</Heading>
  {steps[currentStep].fields.map((field) => (
    <Field key={field.name} label={field.label} htmlFor={field.name} error={field.error} invalid={!!field.error}>
      <Input id={field.name} />
    </Field>
  ))}
  <HStack justify="between" className="mt-4">
    {currentStep > 0 && <Button variant="ghost" onClick={prevStep}>Back</Button>}
    {currentStep < steps.length - 1 ? (
      <Button color="primary" onClick={nextStep}>Next</Button>
    ) : (
      <Button color="primary" onClick={submit}>Submit</Button>
    )}
  </HStack>
</VStack>

3. Focus management on step change

When the step changes, move focus to the step heading so screen reader users know the step changed:

const stepHeadingRef = useRef<HTMLHeadingElement>(null);

function nextStep() {
  if (validateStep(currentStep)) {
    setCurrentStep((s) => s + 1);
    setTimeout(() => stepHeadingRef.current?.focus(), 0);
  }
}

Component Docs

FAQ