For custom radios (or checkboxes), option 2 and 4 are not recommended because screen readers won’t be able to read the default radio element. This also prevents us from using the :focus pseudo-element on the hidden input, so those are out of the picture.

Which leaves us with option 1 and option 3. I sort of like option 1, because clip-path is fun. Clip-path creates a clipping region which defines what portion of an element is visible. This clipping region can be a basic shape or a url referencing a clipping path element.

In this case, using polygon(0 0) is still legitimate (according to the validator I checked), because the specification says:

At least three vertices are required to define a polygon with an area. This means that (for this specification) polygons with less than three vertices (or with three or more vertices arranged to enclose no area) result in an empty float area.

By defining a polygon with only 1 vertex, I’ve created an empty float area. An empty float area (where the shape encloses no area) has no effect on line boxes. Brilliant.

What’s not so brilliant is the sad state of affairs when it comes to browser support for clip-path .

That’s how I decided to go with option 3, a good ole’ opacity: 0 coupled with a position: absolute . Hidden but still focusable, well-supported across browsers. Just what we’re looking for.

Visual indicators on the labels

Adjacent sibling combinators only work forwards and not backwards, so the label has to come after the input element in the source order. Also, the input element must have an id attribute to link it to its label via the for attribute. The HTML for my spruced up radio buttons looks like this:

<input id="nric" type="radio" name="id-type" value="nric" checked>

<label for="nric" class="config-select">

<span class="emoji" role="img" aria-label="Singapore">🇸🇬</span>

<span>NRIC</span>

</label> <input id="ektp" type="radio" name="id-type" value="ektp">

<label for="ektp" class="config-select id-config-wrapper">

<span class="emoji" role="img" aria-label="Indonesia">🇮🇩</span>

<span>eKTP</span>

</label>

And the default radio input was hidden like so:

input[type=radio] {

opacity: 0;

position: absolute;

}

To add the glowy blue halo around focused elements, the CSS is as follows:

input[type=radio]:focus + label {

outline: rgba(77, 97, 171, 0.5) auto 3px;

}

You are free to customise the outline styles however you like

For this particular instance, I chose to use one of the accent colours on the project for the outline, but you don’t necessarily have to use outline to indicate focus state. As long as you got the selector right, you can be free to change background colours, border colours, box shadows, gradients, anything you feel like.

Eric Bailey wrote a comprehensive post on all the different ways you could style your focus styles in his CSS Tricks article published earlier this year. He covers some of the newer CSS selectors like :focus-within and :focus-visible .

Laying out the toggles

This next part has nothing to do with accessibility, but just a quick run-through of how the spruced-up radio buttons were laid out on the page.

As this was a Cordova-based demo meant to be installed on Android tablets, I had no way of knowing if the device was updated to a browser that supported Grid, so it was probably a good idea to start off with a base layout that used Flexbox.

The structure of the page was meant to look something like this:

There’s an extra set of radios that gets toggled

There are 2 styles of radio buttons here, the big, rounded square ones and a smaller secondary set of rectangular ones. Only the first primary radio button has additional options attached, but not the second. So the primary radio buttons also trigger show/hide styling on the secondary radio buttons set.

If we add the secondary radio buttons set to the previous HTML, it’ll now look something like this:

<div class="nric-wrapper">

<input id="nric" type="radio" name="id-type" value="nric" checked>

<label for="nric" class="config-select">

<span class="emoji" role="img" aria-label="Singapore">🇸🇬</span>

<span>NRIC</span>

</label> <div class="inline-radios nric-options">

<input id="single" type="radio" name="front-only" value="true" checked>

<label for="single" class="inline-radio">Front-only</label>

<input id="double" type="radio" name="front-only" value="false">

<label for="double" class="inline-radio">Front & back</label>

</div>

</div> <input id="ektp" type="radio" name="id-type" value="ektp">

<label for="ektp" class="config-select id-config-wrapper">

<span class="emoji" role="img" aria-label="Indonesia">🇮🇩</span>

<span>eKTP</span>

</label>

Showing/hiding the secondary radio buttons set can be done with CSS sibling selectors and pseudo-elements as well. Here the general sibling combinator, ~ , is used instead of the adjacent sibling combinator, + used previously:

.nric-options {

opacity: 0;

} input[value=nric]:checked ~ .nric-options {

opacity: 1;

}

The general layout is flex-based, with the page layout laid out in the column direction, and the top-most set of radios laid out in the row direction. The figure below shows the 2 flex formatting contexts, the outer in purple and the inner in orange.

Multiple flex containers

If you have Firefox Nightly installed, DevTools has a Flexbox inspector tool that provides a visual overlay on the page which shows the flex container and children within that flex formatting context. It’s similar to Firefox’s excellent Grid inspector tool, and you can customise the colour of the overlay.

Firefox Flexbox inspector

A common design pattern for a mobile application is a full-width call to action button right at the bottom of the screen. Flexbox makes it relatively straightforward to have such a layout with its space distribution capabilities.

Setting the flex-direction of the flex container to column , and justify-content to space-between , will result in the first and last flex child flush against the edges of the flex container.

For the specific layout that I wanted, an additional margin: auto was applied to the sets of radio buttons, which distributed the amount of available space around each set equally on all sides.

Wrapping up

Customized radio buttons and checkboxes are a common design pattern found on the web, and it doesn’t take too much to ensure that your beautifully designed toggles are still navigable via keyboard.

Thanks for reading. Feel free to comment below and ask anything! :)

Relevant reading