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); }}