Video examples

iOS Voiceover

Android Talkback

MacOS Voiceover Safari

Code examples

Use semantic HTML

  • This semantic HTML contains all accessibility features by default.
  • It uses CSS pseudo attributes to create the radio indicator, no Javascript.
<fieldset>
  <legend>
    Choose your favorite NATO letter
  </legend>

  <input type="radio" name="nato" id="alphaRadio">
  <label for="alphaRadio">Alpha</label>

  <input type="radio" name="nato" id="bravoRadio">
  <label for="bravoRadio">Bravo</label>

  <input type="radio" name="nato" id="charlieRadio" aria-describedby="hint-charlie" checked>
  <label for="charlieRadio">Charlie</label>
  <div class="hint" id="hint-charlie">This is the best letter</div>
</fieldset>
Choose your favorite NATO letter
This is the best letter

Disabled and focusable radio inputs (preferred)

  • An input using aria-disabled="true will be focusable with the tab key
  • Use JS to preventDefault()
<fieldset>
  <legend>
    Choose your favorite dance
  </legend>

  <input type="radio" name="dance" id="carltonRadio" aria-disabled="true">
  <label for="carltonRadio">Carlton</label>

  <input type="radio" name="dance" id="foxtrotRadio">
  <label for="foxtrotRadio">Foxtrot</label>

  <input type="radio" name="dance" id="tangoRadio" checked>
  <label for="tangoRadio">Tango</label>
</fieldset>
Choose your favorite dance

Required radio inputs

Ensuring all screenreaders indicate radio inputs as being required requires some aria and reinforcement.

  • Use aria-required="true" to indicate the group is required
  • Use aria-invalid="true/false" to indicate an error state
  • Add role="radiogroup" to the <fieldset> to make the aria-required attribute valid
  • Add “Required” as text to the <legend> to ensure compliance across all platforms
<fieldset aria-required="true" 
          aria-invalid="true" 
          role="radiogroup">
  <legend>
    Choose your second favorite NATO letter <span>Required</span>
  </legend>

  <input type="radio" name="natoReq" id="deltaRadioReq">
  <label for="deltaRadioReq">Delta</label>

  <input type="radio" name="natoReq" id="echoRadioReq">
  <label for="echoRadioReq">Echo</label>

  <input type="radio" name="natoReq" id="foxtrotRadioReq">
  <label for="foxtrotRadioReq">Foxtrot</label>
</fieldset>
Choose your second favorite NATO letter Required

Radio button cards

<ul class="cards">
  <li class="card interactive">
    <input type="radio"
           name="radioCards"
           id="deltaRadioCard"
           aria-describedby="hint-deltaRadioCard" >
    <label for="deltaRadioCard">
      Delta
    </label>
    <div class="extended-description"
         id="hint-deltaRadioCard">
      Delta (prounounced: <strong>dell</strong>-tah)
      is the fourth letter of the NATO alphabet.
    </div>
  </li>
  <li class="card interactive">
    <input type="radio"
           name="radioCards"
           id="echoRadioCard"
           aria-describedby="hint-echoRadioCard">
    <label for="echoRadioCard">Echo</label>
    <div class="extended-description"
         id="hint-echoRadioCard">
      Echo (prounounced: <strong>eck</strong>-oh)
      is the fifth letter of the NATO alphabet.
    </div>
  </li>
</ul>
  • Delta (prounounced: dell-tah) is the fourth letter of the NATO alphabet.
  • Echo (prounounced: eck-oh) is the fifth letter of the NATO alphabet.

When you can’t use semantic HTML

This custom button requires extra scripting work for roving tabindex and event listeners.

<custom-label id="labelId">
    Which is your favorite NATO letter:
</custom-label>
<div role="radiogroup" aria-labelledby="labelId">
  <custom-element role="radio" tabindex="-1">
    Alpha
  </custom-element>
  <custom-element role="radio" tabindex="-1">
    Bravo
  </custom-element>
  <custom-element role="radio" tabindex="-1">
    Charlie
  </custom-element>  
</div>

Specialty use cases

Radio mixed with interactive elements

Avoid this pattern when possible! Radio groups are not supposed to consist of nested interactive elements. Radio button focus order is not what you may expect.

  • By default, it is not expected behavior that each radio button can be tabbed to. This is how radio buttons naturally behave
  • As soon as a radio button is selected, the selected radio input receives focus first from the group. As a result screen reader users may not discover a nested control for an option if they start switching between radio buttons alone
  • To try to mitigate screen reader users not discovering the nested controls, describe the fieldset / radiogroup with non-visual text. This can be done with aria-describedby on the fieldset. For example, “Edit controls are available which follow each radio button”

  • Ensure the nested controls also have additional context defined by aria-describedby. This will help screen reader users understand their purpose.
  • Use of the same name attribute is important to link the radio buttons as a programmatic group
  • Keyboard functionality such as arrow up/down/left/right should change the selected radio button.
<fieldset class="checkbox-radio-group" aria-describedby="styled-radio-group-helper-text">
  <legend>Choose your payment method:</legend>
  <!-- Visually hidden helper text describing fieldset -->
  <!-- This text should only be present in the DOM if the radio group has nested controls. Hiding it with CSS display: none; is not enough to hide it from screen readers. Ensure aria-describedby on the fieldset does not point to an ID that is not in the DOM -->
  <span class="hidden" id="styled-radio-group-helper-text">
    Edit controls are available which follow each radio button
  </span>
  <input class="radio" type="radio" name="checkboxRadioGroup2" id="checkboxRadioAlpha2" checked>
  <label for="checkboxRadioAlpha2">
    Alpha
  </label>
  <button type="button" class="tertiary" aria-describedby="checkboxRadioAlpha2">
    Edit
  </button>

  <input class="radio" type="radio" name="checkboxRadioGroup2" id="checkboxRadioBravo2">
  <label for="checkboxRadioBravo2">
    Bravo
  </label>
  <button type="button" class="tertiary" aria-describedby="checkboxRadioBravo2">
    Edit
  </button>

  <input class="radio" type="radio" name="checkboxRadioGroup2" id="checkboxRadioCharlie3">
  <label for="checkboxRadioCharlie3">
    Charlie
  </label>
  <button type="button" class="tertiary" aria-describedby="checkboxRadioCharlie3">
    Edit
  </button>
</fieldset>
Choose your payment method:

Developer notes

Name

  • label text must describe the radio input.
  • Use aria-describedby="hint-id" for hints or additional descriptions
  • aria-label="Radio input purpose" can also be used (as a last resort)

Role

  • By default, semantic HTML radio inputs identify as radio button
  • Use role="radio" for custom elements

Group

  • Semantic HTML
    • <fieldset> must wrap the radio group
    • <legend> must describe the group’s purpose
    • Each <label> must include for="input-id" to be associated with its input
  • Custom elements
    • Use role="radiogroup" to take the place of fieldset
    • Use aria-labelledby="label-id" to associate an element as a label
    • aria-label="Group purpose" can also be used if there’s no label with an ID

State

  • Semantic HTML
    • checked (will be read as “selected” by screen reader)
    • Use the disabled state for inactive buttons
  • Custom element
    • Use aria-checked="true/false" to express state
    • Use aria-disabled="true" to declare inactive elements

Focus

  • Focus must be visible
  • Custom elements will require keyboard event listeners and roving tabindex
  • DO NOT put interactive elements inbetween radio inputs.
    • Performs its purpose across platforms, devices and viewports

Thanks

Further Reading