Accessible Forms — Complete Guide
In this tutorial, you will learn about Accessible Forms. We cover key concepts, practical examples, and best practices to help you master this topic.
Accessible forms use proper labels, clear error handling, logical field grouping, input purpose auto-complete attributes, and a logical focus order to ensure all users can understand and complete them successfully.
What You'll Learn
- How to correctly associate labels with form controls
- How to group related fields with fieldset and legend
- Accessible error validation and messaging
- Input purpose attributes for auto-fill
- Keyboard navigation through forms
- Custom form controls Accessibility
Why It Matters
- Forms are the primary way users interact with websites
- Inaccessible forms prevent users from registering, purchasing, or contacting
- WCAG requires labels, error identification, and input assistance
- Well-designed forms have higher completion rates for all users
Real-World Use
- A checkout form groups billing and shipping sections
- A registration form validates input and announces errors clearly
- A search form has a visible label and autocomplete support
- A contact form uses appropriate input types for phone, email, and date
flowchart LR A[Form Structure] --> B[Labels] A --> C[Input Types] A --> D[Validation] A --> E[Submission] B --> F[for attribute] C --> G[type, autocomplete] D --> H[Error Messages] E --> I[Confirmation]
Accessible Form Fundamentals
An accessible form starts with labels. Every form control must have a label that is programmatically associated with it. The most reliable way is using the <label> element with the for attribute.
Label Association
There are several ways to associate a label with a form control:
- Explicit label (best):
<label for="email">Email</label><input id="email"> - Implicit label:
<label>Email <input></label> - aria-label:
<input aria-label="Email"> - aria-labelledby:
<input aria-labelledby="email-label">
The explicit label with for and id is the most reliable across browsers and assistive technologies.
Input Purpose Attributes
HTML5 introduced the autocomplete attribute that tells browsers and assistive technologies the purpose of each field. This enables password managers, auto-fill, and faster form completion.
<!-- Common autocomplete values -->
<input type="text" name="name" autocomplete="name">
<input type="email" name="email" autocomplete="email">
<input type="tel" name="phone" autocomplete="tel">
<input type="text" name="address" autocomplete="street-address">
<input type="text" name="city" autocomplete="address-level2">
<input type="text" name="country" autocomplete="country-name">
<input type="password" name="password" autocomplete="new-password">
Code Example: Complete Accessible Form
<form action="/register" method="POST" novalidate>
<h1>Create Your Account</h1>
<p>Required fields are marked with <span aria-hidden="true">*</span></p>
<!-- Personal information section -->
<fieldset>
<legend>Personal Information</legend>
<div>
<label for="first-name">
First Name <span aria-hidden="true">*</span>
</label>
<input type="text"
id="first-name"
name="first_name"
autocomplete="given-name"
required
aria-required="true"
aria-describedby="fn-desc fn-error">
<span id="fn-desc" class="hint">Enter your legal first name.</span>
<span id="fn-error" class="error" role="alert" hidden></span>
</div>
<div>
<label for="last-name">
Last Name <span aria-hidden="true">*</span>
</label>
<input type="text"
id="last-name"
name="last_name"
autocomplete="family-name"
required
aria-required="true"
aria-describedby="ln-error">
<span id="ln-error" class="error" role="alert" hidden></span>
</div>
<div>
<label for="email">Email Address <span aria-hidden="true">*</span></label>
<input type="email"
id="email"
name="email"
autocomplete="email"
required
aria-required="true"
aria-describedby="email-error">
<span id="email-error" class="error" role="alert" hidden></span>
</div>
</fieldset>
<!-- Account section -->
<fieldset>
<legend>Account Details</legend>
<div>
<label for="username">Username <span aria-hidden="true">*</span></label>
<input type="text"
id="username"
name="username"
autocomplete="username"
required
aria-required="true"
minlength="3"
aria-describedby="username-hint username-error">
<span id="username-hint" class="hint">At least 3 characters, letters and numbers only.</span>
<span id="username-error" class="error" role="alert" hidden></span>
</div>
<div>
<label for="password">Password <span aria-hidden="true">*</span></label>
<input type="password"
id="password"
name="password"
autocomplete="new-password"
required
aria-required="true"
minlength="8"
aria-describedby="pw-hint pw-error">
<span id="pw-hint" class="hint">At least 8 characters with uppercase, lowercase, and one number.</span>
<span id="pw-error" class="error" role="alert" hidden></span>
</div>
<div>
<label for="password-confirm">Confirm Password <span aria-hidden="true">*</span></label>
<input type="password"
id="password-confirm"
name="password_confirm"
autocomplete="new-password"
required
aria-required="true"
aria-describedby="pw-confirm-error">
<span id="pw-confirm-error" class="error" role="alert" hidden></span>
</div>
</fieldset>
<!-- Preferences section -->
<fieldset>
<legend>Preferences</legend>
<div>
<label for="newsletter">
<input type="checkbox" id="newsletter" name="newsletter" checked>
Subscribe to our newsletter
</label>
</div>
<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>
</fieldset>
<div>
<label for="country">Country</label>
<select id="country" name="country">
<option value="">Select a country</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="UK">United Kingdom</option>
</select>
</div>
</fieldset>
<button type="submit">Create Account</button>
</form>
<style>
.error { color: #C62828; font-size: 0.85rem; display: block; }
.hint { color: #666; font-size: 0.85rem; display: block; }
[aria-invalid="true"] { border-color: #C62828; }
</style>
Expected output: Screen readers navigate through fieldsets and announce each legend. Labels are announced for each input. Hints provide additional context. Errors are announced when validation fails. The autocomplete attributes enable browser auto-fill.
Code Example: Accessible Validation
// Accessible form validation
const form = document.querySelector('form');
form.addEventListener('submit', function(event) {
event.preventDefault();
let hasError = false;
// Clear previous errors
document.querySelectorAll('.error').forEach(el => {
el.hidden = true;
el.textContent = '';
});
document.querySelectorAll('[aria-invalid]').forEach(el => {
el.removeAttribute('aria-invalid');
});
// Validate each field
const fields = [
{ id: 'first-name', test: val => val.trim().length > 0, msg: 'First name is required.' },
{ id: 'last-name', test: val => val.trim().length > 0, msg: 'Last name is required.' },
{ id: 'email', test: val => val.includes('@') && val.includes('.'), msg: 'Enter a valid email address.' },
{ id: 'username', test: val => val.trim().length >= 3, msg: 'Username must be at least 3 characters.' },
{ id: 'password', test: val => val.length >= 8 && /[A-Z]/.test(val) && /[a-z]/.test(val) && /[0-9]/.test(val), msg: 'Password does not meet requirements.' },
{ id: 'password-confirm', test: (val, form) => val === form.querySelector('#password').value, msg: 'Passwords do not match.' }
];
fields.forEach(field => {
const input = document.getElementById(field.id);
const error = document.getElementById(field.id + '-error');
const value = input.value;
// Pass the form as second argument for cross-field validation
if (!field.test(value, form)) {
input.setAttribute('aria-invalid', 'true');
error.textContent = field.msg;
error.hidden = false;
if (!hasError) {
input.focus();
hasError = true;
}
}
});
if (!hasError) {
const confirmation = document.createElement('div');
confirmation.setAttribute('role', 'alert');
confirmation.textContent = 'Account created successfully!';
form.prepend(confirmation);
}
});
Expected output: When submitted with errors, the first invalid field receives focus. Each error appears next to its field and is announced by screen readers via the role="alert" attribute. Valid fields are cleared of previous errors.
Code Example: Custom Checkbox and Radio
<!-- Accessible custom checkbox -->
<style>
.custom-checkbox {
display: inline-flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.custom-checkbox input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.custom-checkbox .checkmark {
width: 20px;
height: 20px;
border: 2px solid #666;
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.custom-checkbox input:focus-visible + .checkmark {
outline: 3px solid #0056B3;
outline-offset: 2px;
}
.custom-checkbox input:checked + .checkmark::after {
content: "✓";
color: white;
font-weight: bold;
}
.custom-checkbox input:checked + .checkmark {
background: #0056B3;
border-color: #0056B3;
}
</style>
<label class="custom-checkbox">
<input type="checkbox" id="terms" required>
<span class="checkmark" aria-hidden="true"></span>
<span>I agree to the <a href="/terms">Terms and Conditions</a></span>
</label>
Expected output: The native checkbox is visually hidden but still focusable and accessible. Screen readers announce it as a checkbox. The visual custom styling replaces the default appearance. The focus indicator is visible for keyboard users.
Common Mistakes
- Missing or incorrect labels — Placeholder text is not a label. It disappears when the user types and fails WCAG. Always use a proper label element.
- Error messages without association — An error message that is not associated with its input (via aria-describedby) is not announced by screen readers.
- Grouping fields without fieldset — Related fields like shipping address or payment method should be wrapped in a fieldset with a legend.
- Relying on color alone for errors — Red text for errors is invisible to colorblind users. Always include an icon or text indicator like "Error:".
- Autocomplete attributes missing — Without autocomplete attributes, password managers and auto-fill cannot populate fields, making forms slower to complete.
- Submit button disabled without explanation — A disabled button that does not explain why prevents users from understanding what they need to fix.
- No confirmation after submission — Users need to know the form was received successfully. Provide a clear confirmation message with role="alert".
Practice Questions
- What is the most reliable way to associate a label with a form control? Using the
<label>element with theforattribute matching the input'sid. - What is the purpose of the
autocompleteattribute? It tells browsers and assistive technologies the purpose of each input field, enabling auto-fill, password managers, and faster form completion. - Why should you use
<fieldset>and<legend>for related form controls? They group related fields together, and screen readers announce the legend when navigating between fields in the group. - How do you mark a field as required accessibly? Use the
requiredattribute (which adds implicit aria-required) and visibly mark the field with an asterisk or "required" text. - Challenge: Build a multi-step checkout form (Shipping, Payment, Review) with full accessibility. Include shipping address fields grouped in a fieldset, payment method with radio buttons, order summary with a data table, and form validation that uses aria-describedby for errors. Test with keyboard only and a screen reader.
FAQ
{{< faq "How do I make a date picker accessible?" "Use `` which has native accessibility. If building a custom date picker, use role=\"dialog\", role=\"grid\", and role=\"gridcell\" with appropriate ARIA states." >}}Mini Project
Build a complete accessible registration form. Include: 3 fieldsets (Personal Info, Account Details, Preferences), proper labels with for attributes, autocomplete on all appropriate fields, real-time validation with aria-describedby error messages, a custom checkbox for terms agreement (visually custom but natively functional), a custom select dropdown (fully keyboard accessible), and a confirmation screen with role="alert" after successful submission. Style the form clearly with proper focus indicators. Test with keyboard-only navigation and a screen reader.
What's Next
Continue with Lesson 15: Accessible Data Tables to learn how to present tabular data accessibly with proper headers, captions, and scope attributes.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro