Skip to main content
Official government website

Form implementation

Technical guidance for form state, validation, submission and error handling.

Type
Adoption guide
On this page

Start with a working HTML form. Native submission, labels, names and input types provide a reliable baseline that can be progressively enhanced. Use the Forms pattern for journey and layout decisions and the Form fields foundation for shared field anatomy.

Start with native form semantics

Use a real <form> with an action, method and submit button. Every submitted control needs a stable name; every visible control needs a label or fieldset legend. A control without a name is not included in FormData or a normal browser submission.

<form action="/applications/contact" method="post" novalidate>
  <div class="govbb-form-group">
    <label class="govbb-label" for="email">Email address</label>
    <input
      class="govbb-input"
      id="email"
      name="email"
      type="email"
      autocomplete="email"
    />
  </div>
  <button class="govbb-button" type="submit">Continue</button>
</form>

Use novalidate when the service renders its own consistent server messages. Keep useful constraints such as type, required, min, max and pattern where they describe the data and support client-side enhancement.

Use uncontrolled fields by default

Most React forms do not need state for every keystroke. Let the browser own ordinary field values and read them with FormData on submission. This keeps rendering simple and preserves native form behavior.

import { Button, Input } from '@govtech-bb/react';
import type { FormEvent } from 'react';

function ContactForm() {
  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const email = String(data.get('email') ?? '');
    // Send email to the service endpoint.
  };

  return (
    <form action="/applications/contact" method="post" onSubmit={submit}>
      <Input
        name="email"
        type="email"
        autoComplete="email"
        label="Email address"
      />
      <Button type="submit">Continue</Button>
    </form>
  );
}

Use controlled state when the value changes another part of the interface, needs live formatting, or must be synchronized outside the form. Do not make a field controlled only to collect its final value.

Model submitted data by name

Treat control names as part of the server contract. Keep them stable, unique within the form, and meaningful. Radio buttons in one group share a name; checkboxes may share a name when the server accepts multiple values.

Use data.get(name) for one value and data.getAll(name) for repeated values. Normalize and validate the result on the server before using it. Never trust a value because it passed a browser constraint or client-side schema.

Validation flow

Use one validation flow regardless of whether submission is enhanced:

  1. accept the submitted values
  2. normalize values without changing their meaning
  3. validate on the server
  4. return field errors keyed by control name
  5. render the same form with submitted values preserved
  6. show and focus an error summary
  7. repeat each error beside its field

Run validation on submit for most fields. Validate while typing only when the feedback is genuinely useful before completion, such as showing which password requirements have been met.

Render field errors

An error must be visible and programmatically connected to its control. Keep useful hint text in place, add the actionable error after it, and point aria-describedby at both IDs.

<div class="govbb-form-group">
  <label class="govbb-label" for="email">Email address</label>
  <span class="govbb-hint" id="email-description"
    >We only use this to reply</span
  >
  <span class="govbb-error-message" id="email-error" role="alert">
    Enter an email address in the correct format
  </span>
  <input
    class="govbb-input"
    id="email"
    name="email"
    type="email"
    value="alex@"
    aria-invalid="true"
    aria-describedby="email-description email-error"
  />
</div>

The React Input, TextArea and Select components accept label, description and error props and wire the generated IDs automatically. Use the lower-level FormGroup, Label, Hint and ErrorMessage primitives when custom composition is necessary.

Add an error summary

Render the summary before the form heading or at the start of the form content. Each item links to the invalid control ID. After an enhanced submission fails, move focus to the summary so keyboard and screen reader users are told what happened.

Do not show the summary on first load. Keep field and summary messages aligned, and list errors in form order rather than the order returned by an API.

Handle server responses

Return structured field errors for validation failures and a separate form-level message when the request itself could not be completed. Do not attach a service outage or authorization failure to an arbitrary field.

Use appropriate HTTP status codes. A normal non-JavaScript submission can render the page directly; an enhanced request can map the same response model into component props. Keep the server response as the source of truth.

For unexpected failures, preserve local values and offer a safe retry. Do not claim success until the server has committed the operation.

Submission state

While a slow request is pending:

  • keep entered values visible
  • indicate that the form is being submitted
  • prevent duplicate requests without trapping the person in a disabled state
  • restore the action if the request fails
  • avoid moving focus until there is a meaningful result

Use an idempotency key for consequential operations such as payments or final applications. Client-side button disabling alone cannot prevent duplicate server work.

Progressive enhancement

The form must remain usable when JavaScript fails or loads slowly. Keep real routes in action, use submit buttons rather than click handlers on generic elements, and apply conditional rules on the server as well as the client.

Enhancement may add inline validation, loading feedback, conditional reveals or asynchronous submission. It must not remove browser navigation, keyboard support, autofill or the ability to recover from an error.

Security and privacy

  • Validate and authorize every submission on the server.
  • Protect state-changing requests against cross-site request forgery.
  • Escape submitted values when rendering them back into HTML.
  • Apply rate limits to authentication and other abuse-sensitive endpoints.
  • Collect only necessary personal information and avoid logging sensitive field values.
  • Use HTTPS and never put passwords or sensitive answers in query strings.

Testing

Test the server-rendered path and the enhanced path. Cover valid submission, each validation rule, multiple simultaneous errors, network failure, duplicate submission, browser back/forward navigation, autofill and restored values.

Add accessibility checks for labels, descriptions, error connections and focus movement. Complete the automated coverage with keyboard, screen reader, zoom and mobile input testing.