Pattern
Form field
The unit every form is built from: a label, optional guidance, a control, and a message. Getting this one wiring right is what makes every form in the product accessible by default.
StructurePermalink to this section
Four parts in a fixed order. The order matters as much as the parts, because it is also the order a screen reader announces them.
Shown in the sidebar and in deployment logs.
- Label — always visible, associated with htmlFor. The required marker is part of it.
- Description — guidance placed before the control, while it can still change what someone types.
- Control — Input, Select, Textarea or any control that accepts the field's wiring props.
- Message — the error or success text, announced after the description.
How the wiring worksPermalink to this section
FormField hands the control everything it needs through a render prop, so the aria-describedby chain is assembled the same way in every form.
import { FormField, Input } from "@borneo/react";
<FormField
id="project-name"
label="Project name"
description="Shown in the sidebar and in deployment logs."
required
error={errors.name}
>
{(field) => (
// field carries id, aria-describedby and aria-invalid.
// Spreading it is what connects the label, description
// and error message to this specific control.
<Input
{...field}
value={value}
onChange={(event) => setValue(event.target.value)}
state={errors.name ? "error" : "default"}
placeholder="Q3 marketing site"
/>
)}
</FormField>StatesPermalink to this section
A field has four states worth designing. Error and success are always a colour plus an icon plus a message — never a colour alone.
Default
Shown in the sidebar.
With description
Enter a complete email address, like ana@borneo.design.
Error
role=alert, aria-invalid
Address verified.
Success
role=status
A complete formPermalink to this section
Submit the form empty to see how errors appear, then fix the fields one at a time. Validation runs on submit, not on every keystroke.
Validation timingPermalink to this section
When a message appears matters as much as what it says.
- Validate on submit first. Reporting an incomplete email while someone is still typing it is noise.
- Once a field has shown an error, re-validate it on change so the error clears as soon as it is fixed.
- Move focus to the first invalid field on a failed submit, and announce how many fields need attention.
- Never disable the submit button to express invalidity — it gives no reason and cannot be focused to ask for one.
- Show success messages only where confirmation is genuinely useful, such as an availability check.
- Keep server errors in the same slot as client errors, so there is one place to look.
Do and don'tPermalink to this section
Shown in the sidebar.
Do — put guidance before the control
Guidance placed above the field is read before typing starts, when it can still change the answer.
Must be at least 12 characters.
Don't — hide requirements until failure
Rules a person needs before typing belong in the description, not in an error they have to trigger first.
Do — mark the shorter set
If most fields are required, mark the optional ones. Marking everything marks nothing.
Don't — use placeholder text as the label
The placeholder disappears as soon as someone types, taking the only description of the field with it.
Responsive behaviorPermalink to this section
Fields are full width in a single column. Pair related short fields side by side above 640px, and never split a field's label from its control.
Used for sign-in and billing receipts.
Never place the label beside the control
Left-aligned labels break down on narrow screens, force a fixed label column, and make long labels wrap awkwardly. Labels sit above their control at every breakpoint.