Accessible Forms
Forms are the most common way sighted, keyboard, and screen-reader users interact with a website—and also the easiest part of a site to make inaccessible. A form that "looks fine" visually can be practically unusable for someone navigating with a keyboard or a screen reader if labels, groupings, and error messages aren't wired up correctly. This page covers the concrete HTML patterns that make forms accessible to everyone.
Every Input Needs a Real Label
<label>.There are two ways to associate a label with an input.
label-association.html
<!-- Method 1: for/id association (most flexible) --> <label for="email">Email address</label> <input type="email" id="email" name="email" /> <!-- Method 2: wrapping (implicit association) --> <label> Email address <input type="email" name="email" /> </label>
for/id pattern is more robust and easier to style independently. It also lets you click the label from anywhere to focus the associated input, which grows the clickable target—especially helpful for checkboxes and radio buttons.placeholder attribute alone. It has low color contrast by default, vanishes on input, and is inconsistently announced by assistive technology. Always pair it with a real <label>.Grouping Related Fields with fieldset and legend
<fieldset> with a <legend> as its first child. Screen readers announce the legend text before each control in the group, so users don't lose context.fieldset-legend.html
<fieldset>
<legend>Preferred contact method</legend>
<label>
<input type="radio" name="contact" value="email" checked />
Email
</label>
<label>
<input type="radio" name="contact" value="phone" />
Phone
</label>
<label>
<input type="radio" name="contact" value="sms" />
Text message
</label>
</fieldset>Without the fieldset, a screen reader landing on the second radio button only announces "Phone, radio button, 2 of 3"—with no idea what question it's answering. With the fieldset, it announces "Preferred contact method, Phone, radio button, 2 of 3."
Associating Error Messages with aria-describedby
aria-describedby. The attribute's value is the id of the element containing the description or error text.error-association.html
<label for="password">Password</label> <input type="password" id="password" name="password" aria-describedby="password-error password-hint" aria-invalid="true" /> <p id="password-hint">Must be at least 8 characters.</p> <p id="password-error" role="alert"> Password is too short. </p>
aria-invalid="true" also tells assistive technology the field currently fails validation.Announcing Required Fields
required attribute does double duty: it triggers native browser validation, and it's announced by screen readers as "required" when the field receives focus. Don't rely on a visual asterisk alone—pair it with the attribute (and ideally text, since a bare asterisk can be ambiguous).required-field.html
<label for="fullname"> Full name <span aria-hidden="true">*</span> <span class="visually-hidden">(required)</span> </label> <input type="text" id="fullname" name="fullname" required />
aria-hidden="true", while a visually-hidden span supplies the same information as real text for screen readers.Accessible Custom Form Controls
<input>, <select>,<button>) come with keyboard support, focus management, and screen-reader semantics built in for free. When a design calls for a custom-styled control—a toggle switch, a combobox, a star rating—build it from a native element first, then layer on CSS. Reaching straight for a <div> with click handlers means rebuilding keyboard support, focus states, and ARIA roles from scratch.custom-toggle.html
<!-- Good: a real checkbox styled to look like a toggle switch --> <label class="toggle"> <input type="checkbox" role="switch" name="notifications" /> <span class="toggle-track" aria-hidden="true"></span> Enable notifications </label>
<div> with an onclick handler is not keyboard-focusable, isn't announced as a control, and doesn't respond to Space or Enter without extra ARIA and JavaScript. Start from a native form element whenever one exists for the interaction you need.Putting It Together
accessible-signup-form.html
<form>
<fieldset>
<legend>Account details</legend>
<div>
<label for="signup-email">Email address</label>
<input
type="email"
id="signup-email"
name="email"
required
aria-describedby="email-hint"
/>
<p id="email-hint">We'll never share your email.</p>
</div>
<div>
<label for="signup-password">Password</label>
<input
type="password"
id="signup-password"
name="password"
required
minlength="8"
aria-describedby="password-hint"
/>
<p id="password-hint">At least 8 characters.</p>
</div>
</fieldset>
<button type="submit">Create account</button>
</form>Quick Reference
Pattern | Purpose |
|---|---|
<label for="id"> | Programmatically associates label text with a control |
<fieldset>/<legend> | Groups related controls and announces group context |
aria-describedby | Links hint/error text to an input for screen readers |
aria-invalid="true" | Flags a field as currently failing validation |
required | Native required-field validation and announcement |
Native elements first | Reuse built-in keyboard support instead of reinventing it |
Every input needs a real, associated <label>—never placeholder text alone.
Group related controls with <fieldset> and <legend>.
Link error and hint text to inputs with aria-describedby.
Prefer native form elements over div-based custom controls.