Preview
Forms pattern
A long-form layout keeps sections, validation, and the action row predictable without hiding the source of an error.
Invite teammate form with grouped fields and inline validation
"use client";
import { useState } from "react";
import type { FormEvent } from "react";
import { Button } from "@phuctech/ui/components/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@phuctech/ui/components/card";
import { Field, FieldError, FieldLabel } from "@phuctech/ui/components/field";
import { Input } from "@phuctech/ui/components/input";
export function InviteForm() {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(email.includes("@") ? "" : "Enter a valid email address.");
}
return (
<Card>
<form onSubmit={handleSubmit} noValidate>
<CardHeader><CardTitle>Invite a teammate</CardTitle></CardHeader>
<CardContent>
<Field invalid={Boolean(error)}>
<FieldLabel htmlFor="invite-email">Email</FieldLabel>
<Input id="invite-email" type="email" value={email} onChange={(event) => setEmail(event.target.value)} aria-invalid={Boolean(error) || undefined} />
<FieldError match={Boolean(error)}>{error}</FieldError>
</Field>
</CardContent>
<CardFooter>
<Button type="submit">Review invitation</Button>
<Button type="button" variant="outline">Cancel</Button>
</CardFooter>
</form>
</Card>
);
}When to use
Use this pattern for a form that collects related information and ends with an explicit review, save, or submit action. Keep domain rules in the screen or feature; the pattern only gives those rules a predictable structure.
Layout
- Group fields by the decision the person is making, then separate groups with a quiet divider.
- Use one visible label per control. Use a two-column row only when the fields remain easy to scan at the smallest supported width.
- Keep the action row together at the end. The primary action says what will happen next; the secondary action cancels or returns without saving.
- For a long form, keep the page or form body scrollable and leave the action row in a stable place. Do not hide the only submit action behind a clipped region.
Validation
Validate at the least noisy moment that still helps the task. The example validates on submit, clears a field error when that field changes, and keeps the message beside the control that needs attention.
"use client";
import { useState } from "react";
import type { FormEvent } from "react";
import { Button } from "@phuctech/ui/components/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@phuctech/ui/components/card";
import { Field, FieldError, FieldLabel } from "@phuctech/ui/components/field";
import { Input } from "@phuctech/ui/components/input";
export function InviteForm() {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(email.includes("@") ? "" : "Enter a valid email address.");
}
return (
<Card>
<form onSubmit={handleSubmit} noValidate>
<CardHeader><CardTitle>Invite a teammate</CardTitle></CardHeader>
<CardContent>
<Field invalid={Boolean(error)}>
<FieldLabel htmlFor="invite-email">Email</FieldLabel>
<Input id="invite-email" type="email" value={email} onChange={(event) => setEmail(event.target.value)} aria-invalid={Boolean(error) || undefined} />
<FieldError match={Boolean(error)}>{error}</FieldError>
</Field>
</CardContent>
<CardFooter>
<Button type="submit">Review invitation</Button>
<Button type="button" variant="outline">Cancel</Button>
</CardFooter>
</form>
</Card>
);
}Accessibility
- Connect every
FieldLabelto the control withhtmlForand a stableid. - Set
aria-invalidonly for an invalid value and include the error ID inaria-describedby. - Use
requiredwhen the field is genuinely required; do not rely on an asterisk without text. - Announce a successful submit with a
role="status"message that does not replace the form context. - Test the complete path with a keyboard: enter a value, submit with errors, correct the field, and submit again.
Do and don’t
Keep errors specific, local to the field, and persistent until the value is corrected or the form is reset.
Put fetch calls or business validation inside Field, or make a placeholder carry the only label.