Accessibility pattern · Forms and flows

Inline validation

A field is checked when it is left, and only if something was typed in it; while an error is being put right, it is checked again on every change, so the fix shows at once. Messages are words with an icon, joined to the field by aria-describedby, and sending the form still lists every problem in a summary that takes focus.

WCAG criteria
7
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Create your account

Every field is required.

We send a link to confirm it.

3 to 20 letters, numbers, full stops or underscores.

At least 10 characters. A few words in a row are easy to remember.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves to the next field. A field you typed in is checked as you leave it; one you only passed through is left alone.
ShiftTabMoves to the previous field, with the same rule.
EnterIn a field, sends the form: every field is checked, and if any are wrong the summary lists them and takes focus.
EnterOn a link in the summary, moves focus to that field.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches Email addressEmail address, edit text, required. We send a link to confirm it.
Tab leaves Email address after typing asha.mail.inEnter an email address with an @, like [email protected]
Focus goes back to Email addressEmail address, edit text, required, invalid entry. We send a link to confirm it. Error: Enter an email address with an @, like [email protected]
Typing the @ puts it rightEmail address looks right
Create account is pressed with fields still wrongThere is a problem with your details, group. Enter your full name. Enter a password.
Every field is right and the form is sentAccount created. This demo sends nothing.

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<form class="ap-inline-validation" data-ap-inline-validation novalidate aria-labelledby="iv-form-name">
  <div class="ap-inline-validation__top">
    <h3 class="ap-inline-validation__name" id="iv-form-name">Create your account</h3>
    <p class="ap-inline-validation__sub">Every field is required.</p>
  </div>

  <div class="ap-inline-validation__summary" id="iv-summary" role="group" aria-labelledby="iv-summary-name" aria-describedby="iv-summary-list" tabindex="-1" hidden>
    <h4 class="ap-inline-validation__summary-name" id="iv-summary-name"><svg class="ap-inline-validation__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg>There is a problem with your details</h4>
    <ul class="ap-inline-validation__summary-list" id="iv-summary-list"></ul>
  </div>

  <div class="ap-inline-validation__body">
    <div class="ap-field ap-inline-validation__field">
      <label class="ap-label" for="iv-name">Full name</label>
      <input class="ap-input" id="iv-name" name="name" type="text" autocomplete="name" spellcheck="false" required />
      <p class="ap-inline-validation__msg" id="iv-name-msg"><svg class="ap-inline-validation__icon ap-inline-validation__icon--bad" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><svg class="ap-inline-validation__icon ap-inline-validation__icon--ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg><span class="ap-inline-validation__vh" data-ap-prefix hidden>Error:</span> <span data-ap-text></span></p>
    </div>

    <div class="ap-field ap-inline-validation__field">
      <label class="ap-label" for="iv-email">Email address</label>
      <p class="ap-hint" id="iv-email-hint">We send a link to confirm it.</p>
      <input class="ap-input" id="iv-email" name="email" type="email" autocomplete="email" spellcheck="false" required aria-describedby="iv-email-hint" />
      <p class="ap-inline-validation__msg" id="iv-email-msg"><svg class="ap-inline-validation__icon ap-inline-validation__icon--bad" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><svg class="ap-inline-validation__icon ap-inline-validation__icon--ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg><span class="ap-inline-validation__vh" data-ap-prefix hidden>Error:</span> <span data-ap-text></span></p>
    </div>

    <div class="ap-field ap-inline-validation__field">
      <label class="ap-label" for="iv-username">Username</label>
      <p class="ap-hint" id="iv-username-hint">3 to 20 letters, numbers, full stops or underscores.</p>
      <input class="ap-input" id="iv-username" name="username" type="text" autocomplete="username" autocapitalize="none" spellcheck="false" required aria-describedby="iv-username-hint" />
      <p class="ap-inline-validation__msg" id="iv-username-msg"><svg class="ap-inline-validation__icon ap-inline-validation__icon--bad" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><svg class="ap-inline-validation__icon ap-inline-validation__icon--ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg><span class="ap-inline-validation__vh" data-ap-prefix hidden>Error:</span> <span data-ap-text></span></p>
    </div>

    <div class="ap-field ap-inline-validation__field">
      <label class="ap-label" for="iv-password">Password</label>
      <p class="ap-hint" id="iv-password-hint">At least 10 characters. A few words in a row are easy to remember.</p>
      <input class="ap-input" id="iv-password" name="password" type="password" autocomplete="new-password" required aria-describedby="iv-password-hint" />
      <p class="ap-inline-validation__msg" id="iv-password-msg"><svg class="ap-inline-validation__icon ap-inline-validation__icon--bad" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><svg class="ap-inline-validation__icon ap-inline-validation__icon--ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg><span class="ap-inline-validation__vh" data-ap-prefix hidden>Error:</span> <span data-ap-text></span></p>
    </div>
  </div>

  <p class="ap-inline-validation__vh" role="status" data-ap-live></p>

  <div class="ap-inline-validation__foot">
    <p class="ap-inline-validation__status" role="status"><svg class="ap-inline-validation__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg><span data-ap-done></span></p>
    <button type="submit" class="ap-btn ap-btn--primary">Create account</button>
  </div>
</form>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Each label is tied to its input, and the hint and the message join its aria-describedby, so they are read with the field rather than only shown beside it.

  • 1.3.5 Identify Input Purpose Level AA

    Name, email, username and new password carry autocomplete tokens, so browsers and password managers can fill them in.

  • 1.4.1 Use of Color Level A

    A problem shows an icon, a message and a thick edge; a tick shows an icon and words. Neither relies on red or green.

  • 3.2.2 On Input Level A

    Leaving a field never moves focus or sends anything: the message appears in its own line under the field.

  • 3.3.1 Error Identification Level A

    A field with a problem is marked aria-invalid and gets its message in text, beside it and in the summary.

  • 3.3.3 Error Suggestion Level AA

    Every message says what to do, with an example where a format matters, like [email protected].

  • 4.1.3 Status Messages Level AA

    A new error, an error put right and a sent form are announced from live regions, without moving focus.

Usage

When to use it

Use it

  • Fields with a format or a rule people can miss without knowing: email addresses, usernames, passwords.
  • Long forms, where finding a slip in the first field only at the end means scrolling back up.

Use something else

  • Short forms of two or three fields: checking when the form is sent is simpler and just as quick.
  • Free text with no format, like a name or a message: a tick there tells people nothing.
  • Checks that need the server on every key press; check when the field is left, or on submit.

Common failures

How it usually goes wrong

  • Errors while the field is first being typed

    Calling an email address wrong after one letter scolds people for a field they have not finished. Here a field is first checked when it is left.

  • Errors on fields people only tabbed through

    Tabbing ahead to see what a form asks is not a mistake. A field that was never typed in stays quiet until the form is sent.

  • Success shown by a green border alone

    Color alone tells nothing to people who cannot see it. Each tick here comes with words, and each error with an icon and a message.

  • A message that is not tied to its field

    Text beside the input but outside its aria-describedby is never read when the field is focused. The script adds the message's id when it appears and removes it when it goes.

  • No summary when the form is sent

    On a long form or a phone, inline messages scroll out of sight. Sending still lists every problem at the top and moves focus there.

  • A layout that jumps under the pointer

    A message that pushes the button down as a field loses focus can make a click land on nothing. Each field keeps a line for its message, and leaving a field for the button skips the check, since sending checks everything.

Notes

Building it

  • Reward early, punish late: once a field has been checked, an error clears the moment it is fixed, but a new error waits until the field is left.
  • One polite live region speaks for the fields: it announces a new error when a field is left and the tick's words when an error is put right. Ticks on fields that were right all along stay silent.
  • Full name gets no tick, because a name has no format to get right; it only says so when it is empty.
  • The summary records the last attempt to send. As in the form errors pattern, it changes only when the form is sent again, so nothing above the field moves while someone types.
  • The taken usernames here are a fixed list. A real check asks the server when the field is left, not on every key press, and the server checks everything again on submit.

Sources: WAI Tutorials: Validating input · WAI Tutorials: User notifications · Understanding SC 3.3.1: Error Identification

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction