Skip to main content
On this page

Forms

Build forms that are quick to complete, keyboard-friendly, and easy to recover from errors.

Overview​

Most of the work in an operational tool happens inside a form. Logging a fault, taking a payment, correcting an account record: it is all someone typing into fields, often while a customer waits.

Simple is best. Most of what follows is about taking things out rather than putting them in.

This pattern covers how a form is designed and structured. For the component API, see Form.

Loading...
Edit live code
() => { // ignore - for live code example only.
  const isValidEmail = (value) => /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(value);

  const Example = () => {
    const [errors, setErrors] = useState([]);

    const onSubmit = (e) => {
      e.preventDefault();
      const newErrors = [];

      const {
        name,
        email,
        terms
      } = Object.fromEntries(new FormData(e.currentTarget));

      if (!name) {
        newErrors.push({ id: 'name', message: 'Enter a name' });
      }
      if (email && !isValidEmail(email)) {
        newErrors.push({ id: 'email', message: 'Enter a valid email address' });
      }
      if (!terms) {
        newErrors.push({
          id: 'terms',
          message: 'You must agree to the terms & conditions',
        });
      }

      setErrors(newErrors);

      if (!newErrors.length) {
        alert('success');
      }
    }

    return (
      <>
        <Heading1>Example form</Heading1>
        <Form errors={errors} onSubmit={onSubmit}>
          <FormFields>
            <TextField
              id="name"
              name="name"
              label="Name"
            />
            <EmailField
              id="email"
              name="email"
              label="Email"
              optional
            />
            <Checkbox
              id="terms"
              name="terms"
              label="I agree to the terms & conditions"
            />
          </FormFields>
          <FormActions>
            <PrimaryCTAButton type="submit">Submit</PrimaryCTAButton>
          </FormActions>
        </Form>
      </>
    );
  }

  return <Example />;
}

Information to collect​

Ask only for what is definitely needed. Every extra field slows a user down and increases the chance of error.

  • Ask only for information that will be used. Challenge requests for data that might be useful 'for reporting later'.
  • Start with questions that let users know if they are not eligible, so you avoid wasting their time.
  • Put questions in a logical order. Payment details come after delivery details, for example.

Form structure​

Single-page forms​

Use when the task is standard, the number of fields is low to moderate, and users need to complete most of them every time. Single-page forms are the fastest for keyboard users and let users reference earlier answers while completing later fields.

Avoid them when the form spans multiple unrelated business concepts, such as logging a fault and taking a payment in the same form.

Multi-step forms​

Use when the task has logically distinct phases, such as Identify customer → Log issue → Process refund. Multi-step forms reduce cognitive overload for complex workflows and allow progress to be saved if the workflow is interrupted.

See Multi-step tasks.

Conditional logic​

Use a conditional reveal when a field is only relevant based on a previous answer. Do not show irrelevant fields greyed out or disabled. Show only what is immediately relevant.

Reveal new fields below the input that triggered them. A required field revealed above the user's current position will be missed, and they will submit the form and get a confusing error.

If an option is unavailable because of conditional logic, explain why in descriptive copy and provide a way to resolve the blocker if one exists.

Layout​

  • Use a single-column layout. Multi-column layouts are error-prone, slower to use, and cause abandonment.
  • Align buttons to the left edge of the last input. This is where users naturally look for them, and it supports screen magnifier users.
  • Use Stack to control vertical spacing between form fields.
  • Use FormFields and FormActions to manage spacing and layout consistently. See Form.

Labels and hints​

  • Give every field a visible label. Sighted users can see them, visually-impaired users can hear them, and motor-impaired users can more easily set focus to the related input.
  • Never use placeholder text as a label or as a place to put hint text. It disappears when the user types and is problematic for screen readers.
  • Place hint text below the label and above the input. Use it when the reason for asking is not obvious, or when users are likely to make a mistake, such as a field with a complex set of formatting rules.
  • Keep hint text concise. It should aid scanning, not slow it down.

Required and optional fields​

Mark the exceptions, not the rule. Fields are required by default, so marking optional fields with the optional prop keeps visual clutter down and draws attention to the few fields users can skip.

Where a form shows required-field markers, it needs a key explaining them at the top. See theming for how to turn marking on, and Form for how the key is rendered.

Field groups​

Use a fieldset to group related fields, such as address or contact information. A legend element describes the group for screen readers and must be the first element within the fieldset.

Fields do not always need grouping, but where they are grouped it should always be done with a fieldset.

Nebula has no standalone fieldset component. Use Card, Stack, and Heading, changing the semantic elements with the as prop from styled-components. There is a worked example in the Card documentation.

Disabled states​

Nebula's fields and buttons have no disabled states. Disabled elements give a poor user experience: screen readers often skip them because they are not focusable, keyboards will not register them, and where they are rendered the contrast is frequently too low to read, leaving it unclear what the user needs to do.

For buttons, use the default state and rely on error handling in your form. This gives users feedback on what they have missed. The one exception is while a form is submitting, where disabling the submit button prevents duplicate submissions.

For fields, reframe the problem. Is there a better way to design the experience that removes the need for a deactivated state? Descriptive copy is often a better way to explain why something is unavailable:

Because you have told us you only want green energy, you cannot select coal as an energy source.

If you would like to select non-green energy instead, you can update your preferences here.

Where a field requires additional information in a particular state, use a conditional reveal rather than a disabled state.

Validation​

Much of the guidance above exists to help users avoid validation errors, but some will encounter them anyway. Make those as painless to fix as possible.

  • Validate on submission, not while the user types. Live validation interrupts users mid-answer.
  • Be tolerant of minor formatting mistakes such as extra spaces, dashes, or slashes. Do the hard work so users do not have to.
  • Add novalidate to the form element to disable the browser's default HTML validation, whose messages are not always helpful or consistent. Form does this for you.
  • Display a list of errors at the top of the form using ErrorSummaryNotification, with each error linking to its field. See Notification.
  • Use concise, plain language and avoid pleasantries like 'please'. See Error messages.

Error types​

Field validation errors​

Use ErrorSummaryNotification when errors relate to specific fields. The summary lists every error and links each one to the relevant field. Inside Form, it automatically receives keyboard focus when it appears, and field errors are routed to the right field using each field's name prop.

General errors​

Use a Notification with variant="error" for errors that do not relate to a specific field, such as an API timeout. Explain what happened and what the user should do next. The generalError prop on Form handles this, including moving focus to the message.

The Form component​

Use Form, FormFields, and FormActions as the default structure for every form. Much of the guidance on this page is already handled there: disabling browser validation, standardising spacing, routing errors to the right field, moving focus to the error summary, and rendering the required-fields key. Building a form any other way means reimplementing all of it.

Only render fields outside Form where there is a clear reason to. See Form for what it handles and what you take on yourself if you do.

Accessibility​

Most of this page is accessibility guidance already, so this is a summary of where it lives rather than a separate set of rules.

  • Every field needs a visible label, and a placeholder is not one. See Labels and hints.
  • Nothing is disabled. Disabled controls are skipped by screen readers, ignored by keyboards, and often too low-contrast to read. See Disabled states for what to do instead.
  • Errors are announced and reachable. ErrorSummaryNotification takes keyboard focus when it appears and links each error to its field, so a screen reader user hears what went wrong and can get to it. See Error types.
  • Browser validation is off, because its messages are inconsistent between browsers and not reliably announced. See Validation.
  • Revealed fields appear below their trigger. A field revealed above the user's current position is missed by anyone moving through the form linearly, which includes keyboard and screen reader users. See Conditional logic.
  • Grouped fields use a real fieldset and legend, so the group is announced as one thing. See Field groups.

For accessibility guidance across Nebula, see the accessibility guide.

References​