Form
React Hook Form integration with accessible label, description, and error message binding.
Examples
Sign-in with validation
A controlled form: submit invalid values to see inline errors, valid ones to succeed.
import { Button } from "@codefast/ui/button";
import { Field, FieldError, FieldLabel } from "@codefast/ui/field";
import { Input } from "@codefast/ui/input";
import { InputPassword } from "@codefast/ui/input-password";
import type { FormEvent } from "react";
import { useState } from "react";
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function FormSignIn() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [touched, setTouched] = useState(false);
const [done, setDone] = useState(false);
const emailInvalid = touched && !EMAIL.test(email);
const passwordInvalid = touched && password.length < 8;
function submit(event: FormEvent<HTMLFormElement>): void {
event.preventDefault();
setTouched(true);
if (EMAIL.test(email) && password.length >= 8) {
setDone(true);
}
}
return (
<form noValidate className="w-full max-w-xs space-y-4" onSubmit={submit}>
<Field>
<FieldLabel htmlFor="signin-email">Email</FieldLabel>
<Input
id="signin-email"
type="email"
placeholder="you@example.com"
value={email}
aria-invalid={emailInvalid || undefined}
onChange={(event) => {
setEmail(event.target.value);
setDone(false);
}}
/>
{emailInvalid ? <FieldError>Enter a valid email.</FieldError> : null}
</Field>
<Field>
<FieldLabel htmlFor="signin-password">Password</FieldLabel>
<InputPassword
id="signin-password"
placeholder="Min. 8 characters"
value={password}
aria-invalid={passwordInvalid || undefined}
onChange={(event) => {
setPassword(event.target.value);
setDone(false);
}}
/>
{passwordInvalid ? <FieldError>At least 8 characters.</FieldError> : null}
</Field>
<Button type="submit" className="w-full">
{done ? "Signed in ✓" : "Sign in"}
</Button>
</form>
);
}
Contact form
Field + Textarea with inline validation and a helper.
import { Button } from "@codefast/ui/button";
import { Field, FieldDescription, FieldError, FieldLabel } from "@codefast/ui/field";
import { Input } from "@codefast/ui/input";
import { Textarea } from "@codefast/ui/textarea";
import type { FormEvent } from "react";
import { useState } from "react";
export function FormContact() {
const [message, setMessage] = useState("");
const [touched, setTouched] = useState(false);
const invalid = touched && message.trim().length < 10;
function submit(event: FormEvent<HTMLFormElement>): void {
event.preventDefault();
setTouched(true);
}
return (
<form noValidate className="w-full max-w-xs space-y-4" onSubmit={submit}>
<Field>
<FieldLabel htmlFor="contact-name">Name</FieldLabel>
<Input id="contact-name" placeholder="Ada Lovelace" />
</Field>
<Field>
<FieldLabel htmlFor="contact-message">Message</FieldLabel>
<Textarea
id="contact-message"
rows={3}
className="resize-none"
placeholder="How can we help?"
value={message}
aria-invalid={invalid || undefined}
onChange={(event) => {
setMessage(event.target.value);
}}
/>
{invalid ? (
<FieldError>Please write at least 10 characters.</FieldError>
) : (
<FieldDescription>We typically reply within a day.</FieldDescription>
)}
</Field>
<Button type="submit" className="w-full">
Send message
</Button>
</form>
);
}
Usage
The minimal import and composition — see Examples below for styled, real-world variants.
import { Button } from "@codefast/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@codefast/ui/form";
import { Input } from "@codefast/ui/input";
import { useForm } from "react-hook-form";
interface FormUsageValues {
username: string;
}
export function FormUsage() {
const form = useForm<FormUsageValues>({ defaultValues: { username: "" } });
function onSubmit(values: FormUsageValues): void {
console.log(values);
}
return (
<Form {...form}>
<form
className="w-full max-w-sm space-y-4"
onSubmit={(event) => {
void form.handleSubmit(onSubmit)(event);
}}
>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="leo-park" {...field} />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
rules={{ required: "Username is required." }}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
Anatomy
How the parts nest — every slot the component exposes, in composition order.
Features
- FormField connects a react-hook-form Controller to a name/control pair and wires the label/description/message ids through context.
- FormControl (a Slot) sets aria-invalid and aria-describedby on whatever control it wraps, pointing at the description and message ids automatically.
- FormMessage renders the current react-hook-form field error, or falls back to its children when the field is valid.
API reference
Props for each part of the component. All native element props are also forwarded.
Form (react-hook-form)
The Form parts bind react-hook-form to accessible markup — see Usage above for the full FormField/FormControl/FormMessage wiring with useForm. The examples below intentionally stay dependency-free (plain state + Field) to show the same pattern without requiring react-hook-form.
FormField{ control, name, render }Connects a control to RHF and wires label, description, and error ids.
FormMessageReactNodeRenders the field’s validation error from RHF state.
Accessibility
Built to be keyboard-navigable and screen-reader friendly out of the box.
- Associate every control with a label, and link errors via aria-describedby.
- Set aria-invalid on a control when its value fails validation.
- Validate on submit (and optionally on blur) — show errors as text, not only colour.
Guidelines
Conventions that keep usage consistent across an app.
Do
- Keep forms short; group related fields with Field / FieldSet.
- Show a clear success state after submit.
Don’t
- Don’t validate aggressively on every keystroke before first submit.
- Don’t rely on placeholder text instead of labels.
Explore further
Ready to integrate?
Follow the Getting Started guide to install @codefast/ui, or browse the full component gallery.