Multi-step Forms

typescript
1import { Form, useFormStep } from '@flint-ui/core'
2
3function MultiStepForm() {
4 const { step, next, back, isLast } = useFormStep({
5 steps: ['account', 'profile', 'confirm'],
6 })
7
8 return (
9 <Form onSubmit={handleSubmit}>
10 {step === 'account' && (
11 <>
12 <Input name="email" label="Email" required />
13 <Input name="password" label="Password" required />
14 <Button onClick={next}>Next</Button>
15 </>
16 )}
17 {step === 'profile' && (
18 <>
19 <Input name="name" label="Name" />
20 <Input name="company" label="Company" />
21 <Button onClick={back}>Back</Button>
22 <Button onClick={next}>Next</Button>
23 </>
24 )}
25 {step === 'confirm' && (
26 <>
27 <FormSummary />
28 <Button onClick={back}>Back</Button>
29 <Button type="submit">Submit</Button>
30 </>
31 )}
32 </Form>
33 )
34}

Step management with useFormStep and shared Form context

Overview

Multi-step forms break complex data collection into manageable steps. Instead of overwhelming users with a single long form, each step focuses on one logical group of fields.

Flint UI's form primitives compose naturally into multi-step patterns without requiring a dedicated wizard component.

Basic Multi-Step Form

typescript
1import { useState } from 'react'
2import { Input, Button } from '@flint-ui/core'
3
4type Step = 'account' | 'profile' | 'confirm'
5
6function SignupWizard() {
7 const [step, setStep] = useState<Step>('account')
8 const [data, setData] = useState({
9 email: '',
10 password: '',
11 name: '',
12 bio: '',
13 })
14
15 const updateField = (field: string, value: string) =>
16 setData(prev => ({ ...prev, [field]: value }))
17
18 return (
19 <form onSubmit={e => e.preventDefault()}>
20 {step === 'account' && (
21 <>
22 <Input
23 label="Email"
24 type="email"
25 value={data.email}
26 onChange={e => updateField('email', e.target.value)}
27 />
28 <Input
29 label="Password"
30 type="password"
31 value={data.password}
32 onChange={e => updateField('password', e.target.value)}
33 />
34 <Button onClick={() => setStep('profile')}>
35 Next
36 </Button>
37 </>
38 )}
39
40 {step === 'profile' && (
41 <>
42 <Input
43 label="Display Name"
44 value={data.name}
45 onChange={e => updateField('name', e.target.value)}
46 />
47 <Input
48 label="Bio"
49 value={data.bio}
50 onChange={e => updateField('bio', e.target.value)}
51 />
52 <Button variant="ghost" onClick={() => setStep('account')}>
53 Back
54 </Button>
55 <Button onClick={() => setStep('confirm')}>
56 Next
57 </Button>
58 </>
59 )}
60
61 {step === 'confirm' && (
62 <>
63 <p>Email: {data.email}</p>
64 <p>Name: {data.name}</p>
65 <Button variant="ghost" onClick={() => setStep('profile')}>
66 Back
67 </Button>
68 <Button tone="primary" type="submit">
69 Create Account
70 </Button>
71 </>
72 )}
73 </form>
74 )
75}

Step Validation

Validate each step before allowing navigation to the next. This prevents users from reaching the confirmation step with invalid data.

typescript
1const validateStep = (step: Step): string[] => {
2 const errors: string[] = []
3 if (step === 'account') {
4 if (!data.email.includes('@')) errors.push('Valid email required')
5 if (data.password.length < 8) errors.push('Password must be 8+ characters')
6 }
7 if (step === 'profile') {
8 if (!data.name.trim()) errors.push('Name is required')
9 }
10 return errors
11}
12
13const goNext = () => {
14 const errors = validateStep(step)
15 if (errors.length === 0) {
16 setStep(nextStep)
17 }
18}

Progress Indicator

Show users where they are in the process. A simple step counter works well for 2-4 steps.

typescript
1function StepIndicator({ current, total }: { current: number; total: number }) {
2 return (
3 <div role="progressbar" aria-valuenow={current} aria-valuemax={total}>
4 {Array.from({ length: total }, (_, i) => (
5 <span
6 key={i}
7 style={{
8 width: 8,
9 height: 8,
10 borderRadius: '50%',
11 background: i <= current ? 'var(--flint-primary)' : 'var(--flint-border)',
12 }}
13 />
14 ))}
15 <span>Step {current + 1} of {total}</span>
16 </div>
17 )
18}

Best Practices

  • Keep each step to 2-4 fields maximum
  • Always provide a back button (except on the first step)
  • Show a summary/confirmation step before final submission
  • Persist form state so users don't lose progress on navigation
  • Use aria-live="polite" to announce step changes to screen readers