patterns
Forms
react-hook-form state, zod schemas, shadcn Form primitives. Label above input, error below, verb in the submit button — never 'Submit.'
Read the full specLive specimen — report intake
Blur-level validation, specific error messages, submit button reflects async state. Try submitting with blank fields to see the rhythm.
const reportIntakeSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters."),
email: z.string().email("That doesn't look like a valid email address."),
area: z.string().min(2, "Enter the area or neighbourhood name."),
details: z.string().min(10, "Give us at least a sentence or two of detail."),
});
const form = useForm<ReportIntakeValues>({
resolver: zodResolver(reportIntakeSchema),
defaultValues: { name: "", email: "", area: "", details: "" },
});
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" autoComplete="email" {...field} />
</FormControl>
<FormDescription>We'll reply from wolfbrain@agentmail.to.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* …more fields… */}
</form>
</Form>Anatomy — four slots per field
Every FormField renders a subset of these. Label and control are mandatory; description and error are contextual.
- FormLabelAlways visible · sentence case · no trailing colon
- FormControlInput / Textarea / Select — wires aria-* automatically
- FormDescriptionOptional helper · muted · above the error
- FormMessageRenders only on error · red · specific
Validation rhythm
Don't validate every keystroke. Wait for blur, then stay live only for the field the user already errored on.
- On blurFirst pass — validate the field the user just left.
- On submitRe-validate everything, surface a summary for ≥5 fields.
- On change (errored fields only)Live correction — lets the user see the error resolve.
Copy — specific, not sterile
Per voice-and-tone §5.3 and §4.4 — say what's wrong, not just that something is.
Avoid
Invalid email
Good
That doesn't look like a valid email address.
Avoid
Required
Good
Email is required.
Avoid
Too short
Good
Password must be at least 8 characters.
Avoid
Submit
Good
Send report / Save changes / Sign in