<fieldset>
    <legend>Favourite Flavour</legend>
    <div class="field">
        <input type="radio" id="flavour-chocolate" name="flavour" value="chocolate" checked />
        <label for="flavour-chocolate">Chocolate</label>
    </div>
    <div class="field">
        <input type="radio" id="flavour-vanilla" name="flavour" value="vanilla" />
        <label for="flavour-vanilla">Vanilla</label>
    </div>
    <div class="field">
        <input type="radio" id="flavour-strawberry" name="flavour" value="strawberry" />
        <label for="flavour-strawberry">Strawberry</label>
    </div>
</fieldset>
{% set ariaDescribedby = '' %}
{% if hint %}
    {% set ariaDescribedby = name + '-hint' %}
{% elif error %}
    {% set ariaDescribedby = name + '-error' %}
{% elif hint and error %}
    {% set ariaDescribedby = name + '-hint' + " " + name + '-error' %}
{% endif %}

<fieldset{% if error %} class="field--error"{% endif %}>
    <legend>{{ legend }}</legend>
    {%- if hint %}
    {% render '@hint', { name: name, content: hint } %}
    {%- endif %}
    {%- for value, label in options -%}
    <div class="field">
        <input type="radio" id="{{ name }}-{{ value }}" name="{{ name }}" value="{{ value }}" {% if ariaDescribedby != '' %}aria-describedby="{{ ariaDescribedby }}" {% endif %}{% if error %} aria-invalid="true"{% endif %} {% if default == value %} checked{% endif %}/>
        <label for="{{ name }}-{{ value }}">{{ label }}</label>
    </div>
    {%- endfor -%}
    {%- if error %}
    {% render '@error', { name: name, content: error } %}
    {%- endif -%}
</fieldset>
{
  "name": "flavour",
  "legend": "Favourite Flavour",
  "hint": false,
  "error": false,
  "default": "chocolate",
  "options": {
    "chocolate": "Chocolate",
    "vanilla": "Vanilla",
    "strawberry": "Strawberry"
  }
}

Radio Buttons

Radio buttons are visually hidden and replaced with custom style via pseudo content attached to the adjacent label:

input[type="radio"] {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

input[type="radio"] + label::before {
    /* Custom radio button style. */
}

Labels and the corresponding radio button element are intended to be wrapped in a containing element of some sort, ideally a paragraph.

A group of radio buttons is wrapped in a fieldset with a legend which names the input group. This technique is derived from WebAIM’s article, Accessible Form Controls.

A hint for the group can be added immediately after the <legend> in a paragraph with a class of field__hint.

A validation error message for the group can be added immediately after the last checkbox in a paragraph with a class of field__error.

Hints and validation error messages must be associated with the radio buttons using unique IDs which can be referenced from the radio buttons’ aria-describedby attributes. For more information, see WebAIM’s form validation error documentation.

Radio buttons with an associated error should also be given an aria-invalid attribute of true.