Creating Accessible Styled Radio Groups

In the previous article, we showed you how to create styled checkboxes while maintaining the accessible attributes of native HTML checkboxes. In this article, we will use the same approach to create accessible, styled radio groups.

The reference to radio groups rather than radio buttons is intentional. Radio buttons, by their very nature, work as a group. A single radio button is insignificant without at least one more radio button that corresponds to the same `name` attribute value, compared to an example checkbox that is not dependent on sibling elements. Defining the group in an accessible way is the only variable to be added to the method we had styled in the checkboxes in the previous article.

What makes radio groups and buttons accessible?

Let’s start by mapping the factors that make radio group accessible and understandable for everyone:

  1. Corresponding radio buttons should be grouped together.
  2. Radio groups should be labeled with a name that describes the essence of the choice that’s in front of the user.

Now let’s map the factors that make the single radio button accessible:

  1. It has to be focusable, so that keyboard only users will be able to reach it.
  2. It should have a discernible focus indication to mark the position on the focus sequence for keyboard users.
  3. It has to programmatically express its role, so that assistive technologies can read and announce it as a radio button.
  4. It has to be properly labeled so that it is understandable and interactable for assistive technologies users.
  5. It has to express its state (checked/unchecked).  

Bringing into practice

The markup:

First, we would like to create the container that will group the radio buttons. Grouping radio buttons helps screenreader users get a better mental image of the data they are required to fill up.

Below are examples for two possible options on how to wrap and label radio groups:
Option 1: use the <fieldset> tag for grouping and <legend> tag for labeling:

<fieldset>
    <legend>Group name</legend>
    <!-- the radio buttons -->
</fieldset>

Option 2: use the WAI ARIA role=”radiogroup” for grouping and the “aria-label” attribute for invisible labeling:

<div role="radiogroup" aria-label="Group name">
    <!-- the radio buttons -->
</div>

Now, for the structure of each radio button, similar to the checkbox, each radio button is assembled out of 3 elements, namely a wrapping element, the radio input and the element that will act as the presentation element. Note that the presentation element has an aria-hidden=”true” attribute so screen readers will ignore it.

<div class="radio-wrapper">
    <input type="radio" name="group-name" />
    <span aria-hidden="true"></span>
</div>

Now let’s put it all together and add labels to each radio button:

<fieldset>
  
    <legend>Group name</legend>
      
    <div class="radio-row">
        <div class="radio-wrapper">
            <input type="radio" name="group" id="radio-1" />
            <span aria-hidden="true"></span>
        </div>
        <label for="radio-1">Radio label one</label>
    </div>

    <div class="radio-row">
        <div class="radio-wrapper">
            <input type="radio" name="group" id="radio-2" />
            <span aria-hidden="true"></span>
        </div>
        <label for="radio-2">Radio label Two</label>
    </div>

    <div class="radio-row">
        <div class="radio-wrapper">
            <input type="radio" name="group" id="radio-3" />
            <span aria-hidden="true"></span>
        </div>
        <label for="radio-3">Radio label Three</label>
    </div>

  </fieldset>

The example above shows you how to name the radio buttons using the <label> element. Any other valid naming method will work here as well. If you are using the “aria-label” or “aria-labelledby” attributes, make sure that it is on the radio button itself.

<div class="radio-row">
    <div class="radio-wrapper">
      <input type="radio" name="group" aria-label="radio name" />
      <span aria-hidden="true"></span>
    </div>
</div>

We have the HTML setting for our radio group, so now we would like to style it according to the design’s guidelines, while we are preserving the native semantics and behaviours of the native HTML “radio” in order to keep it accessible.

The CSS:

Let’s start by styling the wrapper div (.radio-wrapper). In this example the wrapper div is defining the radio button’s dimensions and icon size. This is only a styling choice. The only property here that’s essential to our purpose, is the “position: relative” attribute, because it will allow us to position the [radio] input correctly and stack it on top of the span.

.radio-wrapper {
 	position: relative;
 	font-size: 1rem;
 	height: 3rem;
 	width: 3rem;
}

Next, we are going to style the [type=”radio”] element. 

We need the input element to be invisible since we want to present a styled version of it, but we also need it to be available to assistive technologies and to be able to respond to users’ click (change its state).

Set the width and height of the input to 100% so it will take the full size of its parent. 

Now set its position to be absolute so that we can stack it on top of the <span>. For the same purpose, set its z-index to be “0”.

Finally, set the “opacity” property to “0”. This will hide the radio button’s visually, and still keep it on the accessibility and render trees for keyboards and screen readers.

.radio-wrapper > [type="radio"] {
      cursor: pointer;
      height: 100%;
 	left: 0;
  	margin: 0;
 	opacity: 0;
 	position: absolute;
 	top: 0;
 	width: 100%;
 	z-index: 0;
}

The [radio] input is all set, so now we can actually start styling it.

The essentials for our purpose are: 

position: relative; This has 2 reasons – the first is to be able to set its z-index to be lower than the radio button so that we can be sure it won’t block clicking the radio button itself, and the second is for positioning its “::after” pseudo element, which will contain the fill for the checked button.

z-index: -1; as mentioned above, we need the span to be stacked under the checkbox. 

These z-index values are not binding but it is important to pay attention that the z-index value of the span is lower than the z-index value of the input element.

The rest of the properties are purely for appearance.

.radio-wrapper  > [type="radio"]  + span {
     border: 0.2rem solid  #51c0a8;
     border-radius: 50%;
     display: block;
     height: 100%;
     position: relative;
     width: 100%;
     z-index: -1;
}

For the radio check sign I chose to use the <span>s “::after” pseudo element. We want it to appear only when the radio button is checked, so that we can use the “:checked” pseudo class selector.

.radio-wrapper > [type="radio"]:checked + span::after {
	content: "\2731";
	top: -0.2em;
	left: 0.2em;
	color: #51c0a8;
	font-size: 3em;
	position: absolute;
}

As long as you have added the aria-hidden=”true” to the <span>, and as long as it has sufficient contrast from its background, this part will not affect accessibility, and it only depends on your styling requirements.

The last thing we need to do is to make sure that we have a visual focus indication. Remember that the input is just hidden but it is still on the render and in the focus sequence, so we can use the “:focus” pseudo class in the same way we used “:checked” to change the appearance of the <span>.

.radio-wrapper > [type="radio"]:focus + span {
	box-shadow: 1px 1px 2px 2px #51c0a8;
}

That’s it! Now we have an accessible radio group.

To summarize

  1. Markup: 
    1. Group the radio buttons using <fieldset> element or a <div> with a role=”radiogroup”.
    2. Label the group to give screen reader users the context. For example, you can use the <legend> elements for a visible label or “aria-label” for an invisible label. 
    3. Use an input[type=”radio”] for the functionality and a <span> or <div> for the presentation.
    4. Make the accessibility APIs ignore the presentational element by adding to it aria-hidden=”true”.
  2. Styling:
    1. Make the input[type=”radio”] visually hidden, keep it on the render and accessibility trees (opacity: 0) so that the advantages of its native semantics and accessibility are kept.
    2. Make sure that the input[type=”radio”] is stacked on top of the styling element so that  it catches the mouse’s click events.
    3. Don’t forget to handle the visual focus indication for the benefit of keyboard only users.

See it on Codepen