Skip to main content

Form Accessibility

Labels, fieldsets, error handling, and custom controls for accessible forms.

Core Principles

  1. Every input must have a label — Screen readers need labels to announce field purpose
  2. Use semantic HTML<label>, <fieldset>, <legend> provide proper structure
  3. Provide clear error messages — Users need to understand what went wrong and how to fix it

Label Association

Always associate labels with inputs using for/id attributes:

<!-- ✅ Best: explicit for/id association -->
<label for="email">Email</label>
<input type="email" id="email" name="email">

<!-- ✅ Also OK: wrapping label -->
<label>
Email
<input type="email" name="email">
</label>

<!-- ❌ Bad: placeholder only (disappears when typing) -->
<input type="email" placeholder="Email">

Grouping with Fieldset/Legend

Related fields need <fieldset> + <legend> so screen reader users understand the context:

<!-- Shipping address group -->
<fieldset>
<legend>Shipping Address</legend>
<label for="street">Street</label>
<input type="text" id="street" name="street">
<label for="city">City</label>
<input type="text" id="city" name="city">
</fieldset>

<!-- Radio buttons MUST be grouped -->
<fieldset>
<legend>T-shirt size</legend>
<label><input type="radio" name="size" value="s"> Small</label>
<label><input type="radio" name="size" value="m"> Medium</label>
<label><input type="radio" name="size" value="l"> Large</label>
</fieldset>

Without grouping, screen readers announce "Small, radio button, unchecked" with no context about size options.

Required Fields

<!-- Use required attribute + visible text indicator -->
<label for="email">
Email
<span aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<input type="email" id="email" required aria-required="true">

Error Handling

<label for="email">Email</label>
<input
type="email"
id="email"
aria-required="true"
aria-invalid="true"
aria-describedby="email-error"
>
<span id="email-error" role="alert">
Please enter a valid email address
</span>

Error Display Strategy

PhaseBehavior
First attemptValidate on submit only (non-intrusive)
After first submitAdd blur validation (helpful without annoyance)
After field validatesAdd input validation (catch new mistakes immediately)

Error Summary at Top

For forms with multiple errors, show a summary at the top of the form on submit:

<div role="alert" aria-label="Form errors">
<p>Please fix the following errors:</p>
<ul>
<li><a href="#email">Email: Invalid format</a></li>
<li><a href="#password">Password: Too short</a></li>
</ul>
</div>

React: Accessible Form Field Component

function FormField({ label, type = 'text', required, error, hint }) {
const id = useId();
const hintId = hint ? `${id}-hint` : undefined;
const errorId = error ? `${id}-error` : undefined;
const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined;

return (
<div>
<label htmlFor={id}>
{label}
{required && <span aria-hidden="true"> *</span>}
</label>

<input
id={id}
type={type}
required={required}
aria-required={required}
aria-invalid={!!error}
aria-describedby={describedBy}
/>

{hint && <span id={hintId} className="hint">{hint}</span>}

{error && (
<span id={errorId} role="alert" className="error">
{error}
</span>
)}
</div>
);
}

Custom Controls

Custom dropdowns and toggles need proper ARIA:

<!-- Custom select/listbox -->
<div
role="listbox"
aria-label="Choose a color"
aria-activedescendant="option-blue"
tabindex="0"
>
<div id="option-blue" role="option" aria-selected="true">Blue</div>
<div id="option-red" role="option" aria-selected="false">Red</div>
</div>

<!-- Toggle switch -->
<button
role="switch"
aria-checked="false"
onclick="this.setAttribute('aria-checked', this.getAttribute('aria-checked') === 'true' ? 'false' : 'true')"
>
Dark mode
</button>

Common Pitfalls

  • Relying on placeholder text instead of real labels
  • Showing errors visually without semantic connection (aria-describedby)
  • Creating custom form controls without ARIA support
  • Forgetting keyboard accessibility (Tab, arrow keys, Enter, Escape)
  • Using visual-only indicators for required fields
  • Omitting <fieldset>/<legend> for radio/checkbox groups

Content from Frontend-Master-Prep-Series08-accessibility