Skip to content
AgileLogics
6 min read

Better Forms: Hints, Errors, and Feedback

Prerequisites: Form validation

Built-in validation is a start. Good forms also explain themselves.

Hints and error messages

<label for="pw">New password</label>
<p id="pw-hint">At least 12 characters. A passphrase of several words works well.</p>
<input id="pw" name="password" type="password" autocomplete="new-password"
       minlength="12" required aria-describedby="pw-hint pw-error" aria-invalid="true">
<p id="pw-error">Your password is too short — it has 8 characters.</p>
  • aria-describedby lists the ids of elements that describe the field. Screen readers read the label, then the descriptions: "New password, edit protected, At least 12 characters... Your password is too short...".
  • aria-invalid="true" marks the field as invalid — set it when you show a server-side or custom error, remove it when fixed.
  • Put error text next to the field, in words, not only as a red border.

For long forms, also list all errors at the top after a failed submit, with links to each field.

Autocomplete tokens

autocomplete values let browsers and password managers fill forms correctly:

<form action="/checkout" method="post">
  <fieldset>
    <legend>Shipping address</legend>
    <label for="s-name">Full name</label>
    <input id="s-name" name="name" autocomplete="shipping name">
    <label for="s-street">Street address</label>
    <textarea id="s-street" name="street" autocomplete="shipping street-address"></textarea>
    <label for="s-zip">Postcode</label>
    <input id="s-zip" name="zip" autocomplete="shipping postal-code">
    <label for="s-country">Country</label>
    <input id="s-country" name="country" autocomplete="shipping country-name">
  </fieldset>
  <label for="card">Card number</label>
  <input id="card" name="card" inputmode="numeric" autocomplete="cc-number">
  <button>Pay</button>
</form>

The shipping / billing prefixes separate two addresses. Use autocomplete="off" sparingly — for one-time values like a CAPTCHA — and never on login fields (browsers ignore it there anyway).

disabled, readonly, and fieldset

<form action="/account" method="post">
  <label for="acct">Account number</label>
  <input id="acct" name="acct" value="100234" readonly>
  <fieldset disabled>
    <legend>Business details (upgrade to edit)</legend>
    <label for="vat">VAT number</label>
    <input id="vat" name="vat">
  </fieldset>
  <button>Save</button>
</form>

readonly fields are focusable and submitted. disabled fields — and every control inside a disabled fieldset — can't be focused and aren't submitted.

output, progress, and meter

<form id="calc">
  <input id="a" type="number" value="2" aria-label="First number"> +
  <input id="b" type="number" value="3" aria-label="Second number"> =
  <output for="a b" name="sum">5</output>
</form>

<label for="upload">Upload progress</label>
<progress id="upload" max="100" value="70">70%</progress>

<label for="disk">Disk usage</label>
<meter id="disk" min="0" max="100" low="70" high="90" optimum="10" value="93">93%</meter>
  • output holds the result of a calculation or user action.
  • progress shows how far a task has got (no value = indeterminate, a spinner-like bar).
  • meter shows a measurement within a known range; with low, high, and optimum, browsers color it (this one would be shown as bad).

Controls outside the form

The form attribute connects a control to a form elsewhere on the page:

<form id="newsletter" action="/subscribe" method="post"></form>
<label for="nl-email">Email</label>
<input id="nl-email" name="email" type="email" form="newsletter">
<button form="newsletter">Subscribe</button>

Practice

Add a hint and an error message to an email field so that both are read by screen readers, and mark the field invalid.

Show solution
<label for="em">Email</label>
<input id="em" name="email" type="email" aria-describedby="em-hint em-err" aria-invalid="true">
<p id="em-hint">We'll send your receipt here.</p>
<p id="em-err">Enter an email address like name@example.com.</p>

Related developer tools