We can do quite a lot with CSS to create fancy radio buttons (or checkboxes, for that matter), and that’s awesome. I happened to be working on a Cordova-based demo application, which had a few boolean configuration options, which made sense to be implemented as radio buttons. Plain ole’ radio buttons seem a bit boring, but with a bit of CSS, it’s not too much trouble to spruce things up.

I had recently returned home from Web Directions Code in Melbourne, and there were a number of talks that focused on accessibility. As such, I was particularly cognizant of the fact that default radio buttons worked great with keyboard controls and whatever fanciness I was planned to come up with must not break that. Fortunately, it’s not rocket science to ensure keyboard navigation works with fancy radio buttons.

Today, we’re going to cover the different methods of hiding input elements, their effects on accessibility and how to ensure they remain navigable via keyboard after being styled. There will also be a small bonus section on layout, because after making our radio buttons pretty, we want them to go into the right place.

What we’re building

Simple configuration page
Styled radio buttons

Behind all the styles, this page is made up of 3 sets of 2 radio buttons. The concept behind styling radio buttons hinges on the magic of HTML attributes, which allows us to link input elements to their labels. So you can hide away that default blue circle, spruce up the label element however you want, and still interact with the input element via the label.

Hiding the radio input

Most of us mess up the keyboard navigation portion of things when we hide the input element. There are several ways to make something invisible or hidden:

  1. clip-path: polygon(0 0)
  2. display: none
  3. opacity: 0
  4. visibility: hidden

This article on WebAIM discusses invisible content just for screen reader users, and is a pretty useful read. Of the four techniques I listed above, only option 2 hides the input in a way that doesn’t take up space.

The other 3 techniques render the element invisible, but they still take up the original amount of space they occupied. We’ll have to pair them with a position: absolute so they are taken out of the normal document flow, and the rest of your content isn’t disrupted.

What happens when you hide stuff visually
Effects of the different hiding techniques

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.

Can I Use css-clip-path? Data on support for the css-clip-path feature across the major browsers from caniuse.com.

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>

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

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're free to customise the focus state however you like
Focus state styles

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. Because, I just feel compelled to talk about layout in general.

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
Page layout sketch

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>

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

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

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

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
Flexbox inspector overlays

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

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

Relevant reading