Multi-step Forms
typescript
1import { Form, useFormStep } from '@flint-ui/core'23function MultiStepForm() {4 const { step, next, back, isLast } = useFormStep({5 steps: ['account', 'profile', 'confirm'],6 })78 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'34type Step = 'account' | 'profile' | 'confirm'56function SignupWizard() {7 const [step, setStep] = useState<Step>('account')8 const [data, setData] = useState({9 email: '',10 password: '',11 name: '',12 bio: '',13 })1415 const updateField = (field: string, value: string) =>16 setData(prev => ({ ...prev, [field]: value }))1718 return (19 <form onSubmit={e => e.preventDefault()}>20 {step === 'account' && (21 <>22 <Input23 label="Email"24 type="email"25 value={data.email}26 onChange={e => updateField('email', e.target.value)}27 />28 <Input29 label="Password"30 type="password"31 value={data.password}32 onChange={e => updateField('password', e.target.value)}33 />34 <Button onClick={() => setStep('profile')}>35 Next36 </Button>37 </>38 )}3940 {step === 'profile' && (41 <>42 <Input43 label="Display Name"44 value={data.name}45 onChange={e => updateField('name', e.target.value)}46 />47 <Input48 label="Bio"49 value={data.bio}50 onChange={e => updateField('bio', e.target.value)}51 />52 <Button variant="ghost" onClick={() => setStep('account')}>53 Back54 </Button>55 <Button onClick={() => setStep('confirm')}>56 Next57 </Button>58 </>59 )}6061 {step === 'confirm' && (62 <>63 <p>Email: {data.email}</p>64 <p>Name: {data.name}</p>65 <Button variant="ghost" onClick={() => setStep('profile')}>66 Back67 </Button>68 <Button tone="primary" type="submit">69 Create Account70 </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 errors11}1213const 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 <span6 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