Form Accessibility Patterns
Forms are one of the most common places where accessibility failures occur. Airframe provides patterns and styles that make accessible forms the default, not optional.
Required: Label Association
Section titled “Required: Label Association”Every form input must have an associated label.
Method 1: Explicit Association (Recommended)
Section titled “Method 1: Explicit Association (Recommended)”<div class="af-field">
<label class="af-field__label" for="email">
Email
</label>
<input class="af-input" type="email" id="email" name="email">
</div>Method 2: Implicit Association (Wrapping)
Section titled “Method 2: Implicit Association (Wrapping)”<div class="af-field">
<label class="af-field__label">
Email
<input class="af-input" type="email" name="email">
</label>
</div>Method 3: Classless (Semantic Default)
Section titled “Method 3: Classless (Semantic Default)”<form class="af-form">
<label for="email">
Email
</label>
<input type="email" id="email" name="email">
</form>Error States
Section titled “Error States”Errors must be communicated in multiple ways:
- Visual styling (border color)
aria-invalid="true"attribute- Error message linked via
aria-describedby - Error message text (not color-only)
Complete Error Pattern
Section titled “Complete Error Pattern”<div class="af-field">
<label class="af-field__label" for="email">
Email
</label>
<input
class="af-input"
type="email"
id="email"
name="email"
aria-invalid="true"
aria-describedby="email-error"
>
<div class="af-field__error" id="email-error">
Please enter a valid email address
</div>
</div>What happens:
- ✅ Input gets red border (visual)
- ✅
aria-invalid="true"tells screen readers the field is invalid - ✅
aria-describedbylinks the error message to the input, so it’s read when the input is focused - ✅ Error text is visible (not color-only)
Help Text
Section titled “Help Text”Help text should be linked via aria-describedby:
<div class="af-field">
<label class="af-field__label" for="password">
Password
</label>
<input
class="af-input"
type="password"
id="password"
name="password"
aria-describedby="password-help"
>
<div class="af-field__hint" id="password-help">
Must be at least 8 characters with one number
</div>
</div>Required Fields
Section titled “Required Fields”Required indicators must not be color-only:
<div class="af-field">
<label class="af-field__label" for="name">
Name
<span aria-label="required">
*
</span>
</label>
<input
class="af-input"
type="text"
id="name"
name="name"
required
aria-required="true"
>
</div>Better pattern (more explicit):
<div class="af-field">
<label class="af-field__label" for="name">
Name
<span class="af-text-muted">
(required)
</span>
</label>
<input
class="af-input"
type="text"
id="name"
name="name"
required
aria-required="true"
>
</div>Field Grouping
Section titled “Field Grouping”Use <fieldset> and <legend> for related fields:
<fieldset class="af-fieldset">
<legend class="af-fieldset__legend">
Payment Method
</legend>
<div class="af-field">
<label class="af-field__label">
<input type="radio" name="payment" value="credit" checked>
Credit Card
</label>
</div>
<div class="af-field">
<label class="af-field__label">
<input type="radio" name="payment" value="debit">
Debit Card
</label>
</div>
</fieldset>Checkbox and Radio Patterns
Section titled “Checkbox and Radio Patterns”Single checkbox:
<div class="af-field">
<label class="af-field__label">
<input type="checkbox" name="agree">
I agree to the terms and conditions
</label>
</div>Checkbox group:
<fieldset class="af-fieldset">
<legend class="af-fieldset__legend">
Notifications
</legend>
<div class="af-field">
<label class="af-field__label">
<input type="checkbox" name="notifications" value="email">
Email notifications
</label>
</div>
<div class="af-field">
<label class="af-field__label">
<input type="checkbox" name="notifications" value="sms">
SMS notifications
</label>
</div>
</fieldset>Disabled States
Section titled “Disabled States”Use native disabled attribute:
<input
class="af-input"
type="text"
id="disabled-field"
name="disabled-field"
disabled
aria-disabled="true"
>For links styled as buttons, use aria-disabled:
<a
class="af-btn"
href="#"
aria-disabled="true"
onclick="return false;"
>
Disabled Link Button
</a>Complete Form Example
Section titled “Complete Form Example”<form class="af-form">
<div class="af-field">
<label class="af-field__label" for="full-name">
Full Name
<span class="af-text-muted">
(required)
</span>
</label>
<input
class="af-input"
type="text"
id="full-name"
name="fullName"
required
aria-required="true"
aria-describedby="name-help"
>
<div class="af-field__hint" id="name-help">
Enter your full legal name
</div>
</div>
<div class="af-field">
<label class="af-field__label" for="email">
Email
</label>
<input
class="af-input"
type="email"
id="email"
name="email"
aria-invalid="true"
aria-describedby="email-error"
>
<div class="af-field__error" id="email-error">
Please enter a valid email address
</div>
</div>
<fieldset class="af-fieldset">
<legend class="af-fieldset__legend">
Contact Preferences
</legend>
<div class="af-field">
<label class="af-field__label">
<input type="checkbox" name="preferences" value="email">
Email
</label>
</div>
<div class="af-field">
<label class="af-field__label">
<input type="checkbox" name="preferences" value="phone">
Phone
</label>
</div>
</fieldset>
<button type="submit" class="af-btn">
Submit
</button>
</form>Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”❌ Missing Labels
Section titled “❌ Missing Labels”<!-- ❌ Bad - no label -->
<input type="text" placeholder="Enter name">❌ Color-Only Errors
Section titled “❌ Color-Only Errors”<!-- ❌ Bad - error only shown by red border -->
<input class="af-input" style="border-color: red;">❌ Missing aria-describedby
Section titled “❌ Missing aria-describedby”<!-- ❌ Bad - error not linked to input -->
<input aria-invalid="true">
<div class="af-field__error">
Error message
</div>Required:
<!-- ✅ Good - error linked via aria-describedby -->
<input
aria-invalid="true"
aria-describedby="email-error"
>
<div class="af-field__error" id="email-error">
Error message
</div>❌ Div Buttons
Section titled “❌ Div Buttons”<!-- ❌ Bad - not keyboard accessible -->
<div class="af-btn" onclick="submit()">
Submit
</div>Testing Checklist
Section titled “Testing Checklist”- All inputs have associated labels
- Error messages are linked via
aria-describedby - Invalid fields have
aria-invalid="true" - Required fields have
aria-required="true" - Related fields are grouped with
<fieldset>/<legend> - Required indicators are not color-only
- Form is fully keyboard navigable
- Error messages are visible and descriptive
- Help text is linked via
aria-describedby