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