Cmpnts

Menu

Molecules

Field, Fieldset and Form

What goes around form controls, such as labels, hints, errors, groups of choices and the Form's actions.


We only use it to reply.

How should we reply?
Code
HTML
<form class="form">
  <div class="field">
    <label for="pg-name" class="field__label label">Your name</label>
    <input id="pg-name" type="text" class="input" autocomplete="name" />
  </div>
  <div class="field">
    <label for="pg-email" class="field__label label">Email address</label>
    <p id="pg-email-hint" class="field__hint small-text">We only use it to reply.</p>
    <input id="pg-email" type="email" class="input" autocomplete="email" aria-describedby="pg-email-hint" />
  </div>
  <fieldset class="fieldset">
    <legend class="fieldset__legend label">How should we reply?</legend>
    <div class="choices">
      <label class="choice"><input type="radio" class="radio-input" name="pg-reply" value="email" checked /><span>By email</span></label>
      <label class="choice"><input type="radio" class="radio-input" name="pg-reply" value="post" /><span>By post</span></label>
    </div>
  </fieldset>
  <div class="form__actions button-group">
    <button type="submit" class="button button--strong button--accent"><span>Send</span></button>
  </div>
</form>

Why it exists

A form is where a misunderstanding costs someone their time, so the words around a control matter as much as the control: what’s being asked, why, and how to put it right. These parts give those words a fixed place, in the same order every time.

That order is label, hint, error, control, and it is the order a screen-reader user hears them in. An error comes before its control, so it is heard before the Field is reached, and someone using a magnifier sees it beside the label.

Nothing here writes words for you. “(optional)” goes in the label, written by whoever knows which case is rarer, and an error says how to fix the problem. The Fieldset’s legend is the question, so it is read out when someone enters the group.

When to use

  • A Field for one control with its label, and its hint or error if it has them. Every text control, select and range sits in one.
  • A Fieldset for a group answered together: a set of checkboxes or radios, or an address. Its legend is the question.
  • Choices for the checkboxes or radios in a Fieldset.
  • A Form for the column of fields and Fieldsets, with its actions at the end.

Rules

  • Required and optional are written, never generated. Mark the fewer case in the label, usually <span class="text-muted">(optional)</span>. Required controls keep required, which screen readers announce.
  • Errors are not alerts. Link an error to its control with aria-describedby and set aria-invalid="true". A script that inserts an error after the page loads may add role="alert"; server-rendered errors take no role. Every error starts with a Warning icon, so it is never colour alone.

Keyboard and screen readers

The browser provides all of this for native controls. Script only what it can’t know, such as where to send focus after a failed submit.

  • Tab moves between controls. A set of radios is one stop.
  • Space toggles a Checkbox. Arrow keys move between radios and select as they go.
  • Enter in a single-line Field submits the Form.
  • A screen reader hears the label, “required” from required, the hint and error from aria-describedby, and “invalid” from aria-invalid. It hears the legend on entering a Fieldset.
  • After a failed submit, move focus to the first invalid control. An error inserted by script may take role="alert".

Field

Order: label, hint, error, control. The error comes before the control, so a screen-reader user hears it before reaching the Field and a magnifier user sees it beside the label.

We only use it to reply.

Enter a plot number, like A-14

Every 3 days
Code
HTML
<div class="field">
  <label for="email" class="field__label label">Email address</label>
  <p id="email-hint" class="field__hint small-text">We only use it to reply.</p>
  <input id="email" type="email" class="input" autocomplete="email" aria-describedby="email-hint" />
</div>

<!-- Error -->
<div class="field">
  <label for="plot" class="field__label label">Plot number</label>
  <p id="plot-error" class="field__error small-text">
    <svg class="icon" width="1em" height="1em" aria-hidden="true" focusable="false"><use href="#Warning"/></svg>
    Enter a plot number, like A-14
  </p>
  <input id="plot" class="input" aria-describedby="plot-error" aria-invalid="true" />
</div>

<!-- Optional -->
<div class="field">
  <label for="phone" class="field__label label">Phone <span class="text-muted">(optional)</span></label>
  <input id="phone" type="tel" class="input" autocomplete="tel" />
</div>

<!-- Header: the label with an output or a link at the other end -->
<div class="field">
  <div class="field__header">
    <label for="reminder" class="field__label label">Watering reminder</label>
    <output for="reminder" class="small-text">Every 3 days</output>
  </div>
  <input id="reminder" type="range" class="range" min="1" max="7" value="3" />
</div>

<!-- Inline: the label beside its control; hint and error drop below -->
<div class="field field--inline">
  <label for="packets" class="field__label label">Seed packets</label>
  <input id="packets" type="number" class="input" min="1" value="2" />
</div>

Fieldset

How should we contact you?

Choose all that apply.

Code
HTML
<fieldset class="fieldset" aria-describedby="contact-hint">
  <legend class="fieldset__legend label">How should we contact you?</legend>
  <p id="contact-hint" class="fieldset__hint small-text">Choose all that apply.</p>
  <div class="choices">
    <label class="choice"><input type="checkbox" class="checkbox-input" name="contact" value="email" /><span>Email</span></label>
    <label class="choice"><input type="checkbox" class="checkbox-input" name="contact" value="post" /><span>Post</span></label>
  </div>
</fieldset>
  • A boxed group is a Fieldset inside a .card; the Fieldset has no border option.

Choices

Each .choice is a <label> holding the box and its text; the whole row is the target. Rows are at least --spacing-target-min tall (48px; 32px when the product opts into fine-pointer targets) and only as wide as their text. The box sits centred on the first line.

Notifications
Delivery
Code
HTML
<fieldset class="fieldset">
  <legend class="fieldset__legend label">Notifications</legend>
  <div class="choices">
    <label class="choice">
      <input type="checkbox" class="checkbox-input" name="notify" value="email" checked />
      <span class="choice__label">
        Email
        <span class="small-text text-muted">A weekly digest</span>
      </span>
    </label>
    <label class="choice">
      <input type="checkbox" class="checkbox-input" name="notify" value="text" />
      <span>Text message</span>
    </label>
  </div>
</fieldset>

<fieldset class="fieldset">
  <legend class="fieldset__legend label">Delivery</legend>
  <div class="choices choices--inline">
    <label class="choice"><input type="radio" class="radio-input" name="delivery" value="standard" checked /><span>Standard</span></label>
    <label class="choice"><input type="radio" class="radio-input" name="delivery" value="express" /><span>Express</span></label>
  </div>
</fieldset>

Form

Plot size
Code
HTML
<form class="form">
  <div class="field">
    <label for="reg-name" class="field__label label">Full name</label>
    <input id="reg-name" class="input" autocomplete="name" required />
  </div>
  <div class="field">
    <label for="reg-email" class="field__label label">Email address</label>
    <input id="reg-email" type="email" class="input" autocomplete="email" required />
  </div>
  <fieldset class="fieldset">
    <legend class="fieldset__legend label">Plot size</legend>
    <div class="choices choices--inline">
      <label class="choice"><input type="radio" class="radio-input" name="plot-size" value="half" checked /><span>Half</span></label>
      <label class="choice"><input type="radio" class="radio-input" name="plot-size" value="full" /><span>Full</span></label>
    </div>
  </fieldset>
  <div class="form__actions button-group">
    <button type="submit" class="button button--strong button--accent"><span>Register</span></button>
    <button type="reset" class="button button--subtle"><span>Clear</span></button>
  </div>
</form>
  • The actions are a Button Group; align them with a justify-* utility. In an aligned form they line up with the controls.

Do and Don’t

DoDon’t
Keep a visible label on every control.Use the placeholder as the label; it vanishes as people type.
Write “(optional)” on the few optional fields.Mark every required field with an asterisk and a legend explaining it.
Say how to fix an error: “Enter a plot number, like A-14”.Say only that it’s wrong: “Invalid input”.
Link the hint and error to the control with aria-describedby.Leave the hint as nearby text that screen readers skip.
Ask a group question with a Fieldset and legend.Put a heading above loose checkboxes.
Show errors after someone has had a go at a Field.Show errors on a Form nobody has touched yet.

In Storybook

See also

  • Form Controls: The parts people fill in or set, such as text inputs, selects, checkboxes, switches and sliders.
  • Button: An action on the page, such as saving a Form, opening a Dialog or changing a setting.
  • Button Group: A set of related Buttons laid out together, in a row or a column.

Search the docs