Skip to content

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.

Every form input must have an associated label.

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>
<div class="af-field">
  <label class="af-field__label">
    Email
    <input class="af-input" type="email" name="email">
  </label>
</div>
<form class="af-form">
  <label for="email">
    Email
  </label>
  <input type="email" id="email" name="email">
</form>

Errors must be communicated in multiple ways:

  1. Visual styling (border color)
  2. aria-invalid="true" attribute
  3. Error message linked via aria-describedby
  4. Error message text (not color-only)
<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-describedby links the error message to the input, so it’s read when the input is focused
  • ✅ Error text is visible (not color-only)

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 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>

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>

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>

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>
<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>
<!-- ❌ Bad - no label -->
<input type="text" placeholder="Enter name">
<!-- ❌ Bad - error only shown by red border -->
<input class="af-input" style="border-color: red;">
<!-- ❌ 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>
<!-- ❌ Bad - not keyboard accessible -->
<div class="af-btn" onclick="submit()">
  Submit
</div>
  • 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