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-describedbylists theids 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>
outputholds the result of a calculation or user action.progressshows how far a task has got (novalue= indeterminate, a spinner-like bar).metershows a measurement within a known range; withlow,high, andoptimum, 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>