Skip to main content

@ttoss/react-wizard

A React wizard component for guiding users through multi-step flows with configurable step list layouts.

Installation

pnpm add @ttoss/react-wizard

Usage

import { Wizard } from '@ttoss/react-wizard';

const steps = [
{
title: 'Personal Info',
description: 'Enter your details',
content: <PersonalInfoForm />,
onNext: () => validatePersonalInfo(),
},
{
title: 'Address',
content: <AddressForm />,
},
{
title: 'Review',
content: <ReviewStep />,
},
];

const MyWizard = () => {
return (
<Wizard
steps={steps}
layout="top"
variant="spotlight-accent"
labels={{
previous: 'Voltar',
next: 'Avançar',
finish: 'Concluir',
cancel: 'Sair',
}}
onComplete={() => console.log('Done!')}
onCancel={() => console.log('Cancelled')}
/>
);
};

Labels are customizable, so applications can localize them as needed.

Props

PropTypeDefaultDescription
stepsWizardStep[]Array of step definitions.
layout'top' | 'right' | 'bottom' | 'left''top'Position of the step list relative to content.
variant'spotlight-accent' | 'spotlight-primary' | 'primary' | 'secondary' | 'accent''spotlight-accent'Visual variant for the shell, step accents, and primary action.
onComplete() => voidCalled when the user finishes the last step.
onCancel() => voidCalled on cancel. If omitted, cancel button is hidden.
onStepChange(params: { stepIndex: number }) => voidCalled when the active step changes.
initialStepnumber0The initially active step index.
allowStepClickbooleantrueAllow clicking completed steps to navigate back.
labelsPartial<{ previous: string; next: string; finish: string; cancel: string }>Override the built-in navigation labels.

WizardStep

PropertyTypeDescription
titlestringTitle displayed in the step list.
descriptionstringOptional description below the title.
contentReactNodeContent rendered when the step is active.
onNext() => boolean | Promise<boolean>Validation callback. Return false to prevent advancing.

Layouts

The layout prop controls where the step list appears:

  • top — Horizontal step list above the content (default).
  • bottom — Horizontal step list below the content.
  • left — Vertical step list on the left side.
  • right — Vertical step list on the right side.

useWizard Hook

Access the wizard context from within step content:

import { useWizard } from '@ttoss/react-wizard';

const StepContent = () => {
const {
currentStep,
totalSteps,
goToNext,
goToPrevious,
isLastStep,
setStepValidation,
} = useWizard();

return (
<div>
Step {currentStep + 1} of {totalSteps}
</div>
);
};

useWizard Return Value

PropertyTypeDescription
currentStepnumberCurrent active step index.
totalStepsnumberTotal number of steps.
goToNext() => Promise<void>Navigate to the next step.
goToPrevious() => voidNavigate to the previous step.
goToStep(params: { stepIndex: number }) => voidNavigate to a specific step.
isFirstStepbooleanWhether the wizard is on the first step.
isLastStepbooleanWhether the wizard is on the last step.
getStepStatus(params: { stepIndex: number }) => WizardStepStatusGet the status of a step by index.
setStepValidation(validate: () => boolean | Promise<boolean>) => voidRegister a validation function for the current step content.

Example: Form with Step-by-Step Validation

This example demonstrates using a single form across all wizard steps, with each step validating only its relevant fields using Zod:

import { Wizard } from '@ttoss/react-wizard';
import {
Form,
FormFieldInput,
FormFieldSelect,
FormFieldTextarea,
useForm,
z,
zodResolver,
} from '@ttoss/forms';
import { Button, Box, Text } from '@ttoss/ui';

// Define the complete Zod schema for all steps
const schema = z.object({
// Step 1: Personal Information
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
email: z.string().email('Invalid email address'),

// Step 2: Address
street: z.string().min(1, 'Street is required'),
city: z.string().min(1, 'City is required'),
country: z.string().min(1, 'Country is required'),

// Step 3: Additional Info
phone: z.string().min(10, 'Phone must be at least 10 digits'),
comments: z.string().optional(),
});

type FormData = z.infer<typeof schema>;

const RegistrationWizard = () => {
const formMethods = useForm<FormData>({
mode: 'onChange',
resolver: zodResolver(schema),
});

const { trigger, handleSubmit } = formMethods;

const handleComplete = handleSubmit((data) => {
console.log('Form submitted:', data);
// Handle final submission
});

const steps = [
{
title: 'Personal Info',
description: 'Basic information',
content: (
<Box>
<FormFieldInput
name="firstName"
label="First Name"
placeholder="Enter your first name"
/>
<FormFieldInput
name="lastName"
label="Last Name"
placeholder="Enter your last name"
/>
<FormFieldInput
name="email"
label="Email"
type="email"
placeholder="Enter your email"
/>
</Box>
),
onNext: async () => {
// Validate only Step 1 fields
return await trigger(['firstName', 'lastName', 'email']);
},
},
{
title: 'Address',
description: 'Location details',
content: (
<Box>
<FormFieldInput
name="street"
label="Street"
placeholder="Enter your street"
/>
<FormFieldInput
name="city"
label="City"
placeholder="Enter your city"
/>
<FormFieldSelect
name="country"
label="Country"
placeholder="Select your country"
options={[
{ value: 'us', label: 'United States' },
{ value: 'br', label: 'Brazil' },
{ value: 'uk', label: 'United Kingdom' },
]}
/>
</Box>
),
onNext: async () => {
// Validate only Step 2 fields
return await trigger(['street', 'city', 'country']);
},
},
{
title: 'Additional Info',
description: 'Final details',
content: (
<Box>
<FormFieldInput
name="phone"
label="Phone"
placeholder="Enter your phone number"
/>
<FormFieldTextarea
name="comments"
label="Comments (Optional)"
placeholder="Any additional comments"
/>
</Box>
),
onNext: async () => {
// Validate only Step 3 fields
return await trigger(['phone', 'comments']);
},
},
{
title: 'Review',
description: 'Confirm details',
content: (
<Box>
<Text variant="heading">Review Your Information</Text>
{/* Display summary of all form data */}
</Box>
),
},
];

return (
<Form {...formMethods} onSubmit={handleComplete}>
<Wizard
steps={steps}
layout="top"
onComplete={handleComplete}
onCancel={() => console.log('Wizard cancelled')}
/>
</Form>
);
};

Key Points:

  • Single Form: The <Form> component wraps the entire wizard, maintaining form state across all steps
  • Step-by-Step Validation: Each step's onNext callback uses trigger() to validate only the fields relevant to that step
  • Zod Schema: Define the complete schema upfront with all fields from all steps
  • Form Methods: Access trigger from useForm to programmatically validate specific fields
  • Progressive Flow: Users can only advance to the next step after passing validation for current step fields

Example: Complex Component with Internal Validation

This example shows how a complex component can use useWizard to handle its own validation directly:

import { Wizard, useWizard } from '@ttoss/react-wizard';
import { Box, Text } from '@ttoss/ui';
import { useState, useEffect } from 'react';

// Complex component with internal validation using useWizard
const ComplexForm = () => {
const { setStepValidation } = useWizard();

const [formData, setFormData] = useState({
username: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});

// Register validation function with the wizard
useEffect(() => {
const validate = async () => {
const newErrors: Record<string, string> = {};

// Complex validation logic
if (!formData.username || formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
}

if (!formData.password || formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}

if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

setStepValidation(validate);
}, [formData, setStepValidation]);

return (
<Box>
<Box>
<Text>Username</Text>
<input
value={formData.username}
onChange={(e) =>
setFormData({ ...formData, username: e.target.value })
}
/>
{errors.username && <Text color="red">{errors.username}</Text>}
</Box>

<Box>
<Text>Password</Text>
<input
type="password"
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
/>
{errors.password && <Text color="red">{errors.password}</Text>}
</Box>

<Box>
<Text>Confirm Password</Text>
<input
type="password"
value={formData.confirmPassword}
onChange={(e) =>
setFormData({ ...formData, confirmPassword: e.target.value })
}
/>
{errors.confirmPassword && (
<Text color="red">{errors.confirmPassword}</Text>
)}
</Box>
</Box>
);
};

const WizardWithComplexComponent = () => {
const steps = [
{
title: 'Account Setup',
description: 'Create your account',
content: <ComplexForm />,
},
{
title: 'Profile',
content: <Box>Profile information...</Box>,
},
{
title: 'Complete',
content: <Box>Setup complete!</Box>,
},
];

return (
<Wizard
steps={steps}
layout="top"
onComplete={() => console.log('Wizard complete!')}
/>
);
};

Key Points:

  • useWizard Hook: The complex component uses setStepValidation from useWizard to register its validation function
  • Automatic Integration: The wizard automatically calls the registered validation when the user tries to advance
  • No Refs Needed: Simpler than using refs and forwardRef patterns
  • Self-Contained: The component manages its own state and validation logic
  • Dynamic Updates: The validation function updates when formData changes