Form Patterns

Form Patterns

Validation, submission, and error handling — the right way.

Basic Form

typescript
1import { Form, Input, Select, Button } from '@flint-ui/core'
2
3interface SignUpData {
4 name: string
5 email: string
6 role: string
7}
8
9function 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 }
17
18 return (
19 <Form<SignUpData> onSubmit={handleSubmit}>
20 <Input
21 name="name"
22 label="Full Name"
23 required
24 minLength={2}
25 />
26 <Input
27 name="email"
28 label="Email"
29 type="email"
30 required
31 validate={(v) =>
32 /^[^@]+@[^@]+\.[^@]+$/.test(v) || 'Invalid email'
33 }
34 />
35 <Select
36 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.

StrategyWhen to UseUX Impact
onChangeFormat checks (email, phone)Instant feedback, can feel aggressive
onBlurField-level validationGood balance of speed and calm
onSubmitComplex cross-field rulesMinimal 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'
2
3function ErrorSummary() {
4 const { errors, isSubmitted } = useFormContext()
5 if (!isSubmitted || errors.length === 0) return null
6
7 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}
20
21function 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'
2
3function SubmitButton() {
4 const { isSubmitting, isValid } = useFormContext()
5 return (
6 <Button
7 type="submit"
8 disabled={isSubmitting || !isValid}
9 loading={isSubmitting}
10 >
11 {isSubmitting ? 'Saving...' : 'Save Changes'}
12 </Button>
13 )
14}
15
16function 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 fields
24 return err.fieldErrors
25 }
26 toast.error('Something went wrong. Please try again.')
27 }
28 }
29
30 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