Form Patterns
Form Patterns
Validation, submission, and error handling — the right way.
Basic Form
typescript
1import { Form, Input, Select, Button } from '@flint-ui/core'23interface SignUpData {4 name: string5 email: string6 role: string7}89function SignUpForm() {10 const handleSubmit = async (data: SignUpData) => {11 const res = await fetch('/api/signup', {12 method: 'POST',13 body: JSON.stringify(data),14 })15 if (!res.ok) throw new Error('Signup failed')16 }1718 return (19 <Form<SignUpData> onSubmit={handleSubmit}>20 <Input21 name="name"22 label="Full Name"23 required24 minLength={2}25 />26 <Input27 name="email"28 label="Email"29 type="email"30 required31 validate={(v) =>32 /^[^@]+@[^@]+\.[^@]+$/.test(v) || 'Invalid email'33 }34 />35 <Select36 name="role"37 label="Role"38 options={[39 { value: 'dev', label: 'Developer' },40 { value: 'designer', label: 'Designer' },41 { value: 'pm', label: 'Product Manager' },42 ]}43 />44 <Button type="submit">Create Account</Button>45 </Form>46 )47}
Complete sign-up form with coordinated validation
Validation Strategies
Flint UI supports three validation strategies. Each trades off responsiveness against intrusiveness — pick the right one for each field.
| Strategy | When to Use | UX Impact |
|---|---|---|
| onChange | Format checks (email, phone) | Instant feedback, can feel aggressive |
| onBlur | Field-level validation | Good balance of speed and calm |
| onSubmit | Complex cross-field rules | Minimal interruption, delayed feedback |
Recommendation: Use onBlur for most fields, onChange for format masks, onSubmit for cross-field rules like "password must match."
Error Handling
typescript
1import { Form, Input, useFormContext } from '@flint-ui/core'23function ErrorSummary() {4 const { errors, isSubmitted } = useFormContext()5 if (!isSubmitted || errors.length === 0) return null67 return (8 <div role="alert" aria-live="polite">9 <p>{errors.length} field(s) need attention:</p>10 <ul>11 {errors.map((err) => (12 <li key={err.field}>13 <a href={`#field-${err.field}`}>{err.message}</a>14 </li>15 ))}16 </ul>17 </div>18 )19}2021function MyForm() {22 return (23 <Form onSubmit={handleSubmit}>24 <ErrorSummary />25 <Input name="email" label="Email" required />26 <Input name="password" label="Password" required minLength={8} />27 <Button type="submit">Sign In</Button>28 </Form>29 )30}
Coordinated error display with Form context
Async Submission
typescript
1import { Form, Button, useFormContext } from '@flint-ui/core'23function SubmitButton() {4 const { isSubmitting, isValid } = useFormContext()5 return (6 <Button7 type="submit"8 disabled={isSubmitting || !isValid}9 loading={isSubmitting}10 >11 {isSubmitting ? 'Saving...' : 'Save Changes'}12 </Button>13 )14}1516function EditProfile() {17 const handleSubmit = async (data: ProfileData) => {18 try {19 await api.updateProfile(data)20 toast.success('Profile updated')21 } catch (err) {22 if (err instanceof ValidationError) {23 // Map server errors back to fields24 return err.fieldErrors25 }26 toast.error('Something went wrong. Please try again.')27 }28 }2930 return (31 <Form<ProfileData> onSubmit={handleSubmit}>32 <Input name="displayName" label="Display Name" />33 <Input name="bio" label="Bio" />34 <SubmitButton />35 </Form>36 )37}
Form submission with loading state and error recovery