Skip to content

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.

Built fromLabelInputSelectTextareaFormField

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.

app/projects/new/project-name-field.tsx
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

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.

Shown in the sidebar and in deployment logs.

Deployment failures are sent here.

Visible to everyone in the workspace.

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.

Doput guidance before the control

Guidance placed above the field is read before typing starts, when it can still change the answer.

Don'thide requirements until failure

Rules a person needs before typing belong in the description, not in an error they have to trigger first.

Domark the shorter set

If most fields are required, mark the optional ones. Marking everything marks nothing.

Don'tuse 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.