Create custom keyboard accessible radio buttons
Lindsey Kopacz

Lindsey Kopacz @lkopacz

About: I'm a self-taught Front End & JS Dev and professional learner with accessibility expertise. I'm passionate about breaking down concepts into relatable concepts, making it more approachable.

Joined:
Oct 2, 2018

Create custom keyboard accessible radio buttons

Publish Date: Aug 5 '19
80 11

Originally posted on a11ywithlindsey.com.

Hey friends! Today we'll be creating custom keyboard accessible radio buttons! This blog post is a follow-up post from my accessible checkboxes post.

We'll go over:

  1. The markup
  2. Creating a pseudo-element on the label in CSS
  3. Add "selected" styling in CSS
  4. Add focus styling

Starting out

I decided to create a simple group of radio buttons asking what your favorite animal is

<fieldset>
  <legend>What is your favorite Wild Animal?</legend>
  <div class="radio-wrapper">
    <input type="radio" name="animal" id="elephant" />
    <label for="elephant">Elephant</label>
  </div>
  <div class="radio-wrapper">
    <input type="radio" name="animal" id="monkey" />
    <label for="monkey">Monkey</label>
  </div>
  <div class="radio-wrapper">
    <input type="radio" name="animal" id="cheetah" />
    <label for="cheetah">Cheetah</label>
  </div>
  <div class="radio-wrapper">
    <input type="radio" name="animal" id="giraffe" />
    <label for="giraffe">Giraffe</label>
  </div>
</fieldset>
Enter fullscreen mode Exit fullscreen mode

The fieldset groups all the radio buttons together logically. The radios inputs are all options to the question in the legend. Also, remember to associate those form labels with the radio buttons!

A Fieldset with the question 'What is your favorite Wild Animal?' with four options: Elephant, Monkey, Cheetah, Giraffe.

I'm going to add some straightforward CSS to clean it up a bit.

@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');

* {
  font-family: 'Roboto', sans-serif;
}

fieldset {
  border: none;
}
Enter fullscreen mode Exit fullscreen mode

I didn’t do anything much here; I added a font and took away the border from the fieldset.

The fieldset with a sans serif font and no outline on the fieldset

Now let's get to the fun part! Styling these radio buttons!

Creating a pseudo-element on the label

First thing I am going to do is add a ::before pseudo-element on the label element. I'm going to start with something basic first.

$muted-red: #db3846;

input[type='radio'] {
  + label {
    position: relative;
    cursor: pointer;
    margin-left: 20px; /* This will be adjusted */

    &::before {
      content: '';
      position: absolute;
      left: -22px; /* This will be adjusted */
      width: 20px;
      height: 20px;
      background: $muted-red;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The radio buttons won't look like anything much right now. We only want to see the radio buttons to ensure we are replicating the HTML functionality.

Radio buttons with a red box between the label and the button.

I'm going to add a teensy amount of margin on the .radio-wrapper.

$muted-red: #db3846;

+ .radio-wrapper {
+ margin: 0.5rem 0;
+ }

input[type='radio'] {
  + label {
    position: relative;
    cursor: pointer;
    margin-left: 20px; /* This will be adjusted */

    &::before {
      content: '';
      position: absolute;
      left: -24px; /* This will be adjusted */
      width: 18px;
      height: 18px;
      background: $muted-red;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Radio buttons with a red box between the label and the button, with extra space below each field.

Now let's remove that background color and round out the edges.

input[type='radio'] {
  + label {
    position: relative;
    cursor: pointer;
    margin-left: 20px; /* This will be adjusted */

    &::before {
      content: '';
      position: absolute;
      left: -24px; /* This will be adjusted */
+     border-radius: 50%;
+     border: 1px solid #6f686a;
      width: 18px;
      height: 18px;
+     background: transparent;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

As a note, I am going to leave the standard radio buttons for debugging purposes.

Radio buttons with a large circle between the labels.

Add :checked styling in CSS

If you've read my post on keyboard accessible checkboxes you know about the :checked pseudo-class. First, we need to put add an ::after pseudo-element on the label.

input[type='radio'] {
  + label {
    position: relative;
    cursor: pointer;
    margin-left: 20px; /* This will be adjusted */

    &::before {
      content: '';
      position: absolute;
      left: -24px; /* This will be adjusted */
      border-radius: 50%;
      border: 1px solid #6f686a;
      width: 18px;
      height: 18px;
      background: transparent;
    }

+   &::after {
+     content: '';
+     position: absolute;
+     left: -20px;
+     top: 4px;
+     border-radius: 50%;
+     width: 12px;
+     height: 12px;
+     background: $muted-red;
+   }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, this is what that looks like:

Radio buttons with an outlined red circle between the labels.

Now we have the styling in place. Let's only add the background of the ::after pseudo-element when the radio input is :checked.

input[type='radio'] {
  + label {
    &::after {
      content: '';
      position: absolute;
      left: -20px;
      top: 4px;
      border-radius: 50%;
      width: 12px;
      height: 12px;
    }
  }

+ &:checked {
+   + label::after {
+     background: $muted-red;
+   }
+ }
}
Enter fullscreen mode Exit fullscreen mode

So now if I select a radio button, it'll have a background color!

A selected radio button with an outlined red circle, indicating it is selected.

If you notice, though, there is no focus styling. Let's focus on that next (see what I did there)

Add focus styling

If I were to hide the radio button, you would have no idea if I focused on it.

A focused radio button with an outlined circle.

input[type='radio'] {
  &:focus {
    + label::before {
      box-shadow: 0 0px 8px $muted-red;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

I decided to add a similar muted red for the focus styling.

A focused radio button with an outlined red circle.

To finish up, I will:

  • remove the opacity from the radio button itself (the input)
  • remove the margin-left from the label!
input[type='radio'] {
  opacity: 0;

  + label {
    position: relative;
    cursor: pointer;
  }
}
Enter fullscreen mode Exit fullscreen mode

And Voilà!

Gif going through custom radio buttons.

Conclusion

When we make custom radio buttons, we have to make sure we account for the following:

  1. Creating proper HTML structure with associated form labels!
  2. Using pseudo-elements to create the custom-styled element
  3. Accounting for the :checked pseudo-class
  4. Ensuring you can focus on the new radio button
  5. Use opacity: 0 to hide the radio button

If you want to play around with it, here is the finished CodePen!

Stay in touch! If you liked this article:

  • Let me know on Twitter and share this article with your friends! Also, feel free to tweet me any follow up questions or thoughts.
  • Support me on patreon! If you like my work, consider making a $1 monthly pledge. You’ll be able to vote on future blog posts if you make a \$5 pledge or higher! I also do a monthly Ask Me Anything Session for all Patrons!
  • Be the first to learn about my posts for more accessibility funsies!

Cheers! Have a great week!

Comments 11 total

  • Andrew Bone
    Andrew BoneAug 5, 2019

    Is there a reason you went for wrapping each input-label combo in a div rather than using the label element?

    <div class="radio-wrapper">
      <input type="radio" name="animal" id="elephant" />
      <label for="elephant">Elephant</label>
    </div>
    
    

    Rather than

    <label class="radio-wrapper">
      <input type="radio" name="animal" />
      Elephant
    </label>
    
    

    Or is it a simple case of either will do but you went with the div? 🙂

    • Lindsey Kopacz
      Lindsey KopaczAug 5, 2019

      Because I wanted to.

      • Andrew Bone
        Andrew BoneAug 5, 2019

        Cool, thanks 🙂

        Was just checking there wasn't some reason I wasn't aware of 🙂

    • Lindsey Kopacz
      Lindsey KopaczAug 6, 2019

      I would take a look at the responses to this. I prefer explicit association but others put their reasons in.

    • Elizabeth Schafer
      Elizabeth SchaferAug 7, 2019

      One reason to do it this way is that explicit labels (input+label combo) have better support with assistive tech. Implicit labels (label wrapping an input) don't work with Dragon Naturally Speaking.

      a11ysupport.io/tech/html/label_ele...

  • Mike Wheaton
    Mike WheatonAug 6, 2019

    Thanks for this! I'm curious about opacity: 0 instead of display: none for the original radio button. Is this because screen readers are still using the original/hidden radio button?

  • Elizabeth Schafer
    Elizabeth SchaferAug 7, 2019

    I love the suggestions to keep the native radio buttons visible while you're working on a custom replacement and to use the :checked pseudo-class to style selected radio buttons. It's really easy to make custom radio buttons that are accidentally out of sync with the native controls.

  • Eli Bierman
    Eli BiermanAug 7, 2019

    Using this for some radio buttons right now, thank you for the helpful guide!

  • Anton Korzunov
    Anton KorzunovAug 10, 2019

    Quite similar to mine HTML State.

    And there is one moment I would like to change in your example - please don't use position:absolute to position "radio" visuals - it's not scalable.

    Let's try to do the same, but with position:static

Add comment