Pure CSS: Accessible Checkboxes and Radios Buttons

Aditya Bhandari
Feb 3, 2017 · 4 min read

If you are reading this article, you know that adding custom styles to default checkboxes and radio buttons using just CSS is a pain. If you stick with the default options, they look different on different browsers. There are several solutions for getting around this problem using things such as vendor prefixes, background images, unicode characters, and icon fonts. But most of these solutions have issues of their own or are not consistent across all browsers. In addition, very few of these solutions focus on accessibility.

I also found that using any one of the above solutions doesn’t give me the look and feel that I want. But I did find a simple solution that doesn’t compromise the look and feel, and maintains accessibility.

So let’s get started! We will use the following:

Almost all browsers (IE 10 and above) have good support for the three items above.

I will demonstrate a solution for checkboxes here, but radio buttons use the same approach with slightly different styles.


<div class="checkbox">
<input type="checkbox" id="checkbox_1">
<label for="checkbox_1">Pure CSS Checkbox</label>

The outer .checkbox wrapper is to encapsulate the input and label so that we know this chunk of the markup represents a checkbox.

The following things are required to make the checkbox work:

  • The label must appear after the <input type="checkbox">
  • The id attribute is required on the <input type="checkbox">
  • The for attribute is required on the label

The reasons for these requirements will be explained in Step 3 of this article.

Step 1: Hiding the default checkbox without losing accessibility

Because we can’t style the default checkbox using CSS, we need to hide it. There are several ways to hide the <input type="checkbox"> :

  • Use display: none
  • Use visibility: hidden
  • Use opacity: 0
  • Position it off the screen using position: absolute and an insanely big value like left: -9999px

Here is an interesting article that covers some of these methods: http://webaim.org/techniques/css/invisiblecontent/. We don’t want to use the first two methods because a screen reader won’t be able to read the default checkbox. I like the third method best because we don’t necessarily have to position the checkbox off the screen for our use case. It can remain invisible at its current location.

Important Note: Although the hidden input is still accessible, we need to add :focus styles for users to easily detect it. We will do that in Step 4.

So let’s hide the default checkbox using opacity: 0:

.checkbox input[type="checkbox"] {
opacity: 0;

This is what we see after the first step:

Hiding the default checkbox but not making it inaccessible

Step 2: Creating a fake checkbox using pseudo-elements

Using the ::before and ::after pseudo-elements, we can create a fake checkbox. We’ll use the ::before element to create the outer box and the ::after element to create the checkmark. We’ll position these elements before the text in the label.

I’ve divided this step into three parts.

Part 1: Creating the outer box

We’ll make the ::before pseudo-element an inline-block element and assign it a height and a width. We’ll add a border to make it visible. You can also add your own custom styles instead of the border.

.checkbox label::before{
content: "";
display: inline-block;

height: 16px;
width: 16px;

border: 1px solid;

Note: content: "" is required to make the pseudo-element visible. If the content is not set, the pseudo-element will not be rendered.

Styling the outer box of the fake checkbox

Part 2: Creating the checkmark

We’ll make the ::after pseudo-element an inline-block and assign it a height and a width. We’ll also add custom styles to its left and bottom borders and rotate it by 45 degrees to make it look like a checkmark.

.checkbox label::after {
content: "";
display: inline-block;
height: 6px;
width: 9px;
border-left: 2px solid;
border-bottom: 2px solid;

transform: rotate(-45deg);
Creating the checkmark for the fake checkbox

Part 3: Positioning the outer box and checkmark

We’ll use absolute positioning to position both pseudo-elements relative to the text in the label.

.checkbox label {
position: relative;
.checkbox label::before,
.checkbox label::after {
position: absolute;
.checkbox label::before {
top: 3px;
.checkbox label::after {
left: 4px;
top: 7px;
Positioning the fake checkbox relative to the label

Step 3: Making the checkmark appear and disappear based on the checkbox state

Because we use the id attribute on the <input type="checkbox"> and the for attribute on the label, its possible to toggle the state of the default checkbox by clicking the label. And the reason we placed the label after the <input type="checkbox"> is so that we can leverage the :checked state of the <input type="checkbox"> to style the ::after pseudo-element (checkmark) of the label with the help of an + adjacent sibling selector as shown below:

/*Hide the checkmark by default*/
.checkbox input[type="checkbox"] + label::after {
content: none;
/*Unhide the checkmark on the checked state*/
.checkbox input[type="checkbox"]:checked + label::after {
content: "";
Making the checkmark appear/disappear based on the :checked state of the default checkbox

We now have a functional checkbox but remember that its still not accessible.

Step 4: Adding the focus styles to make the checkbox accessible

Using the :focus selector on the <input type="checkbox">, we can again use the + adjacent sibling selector to give the ::before pseudo-element (outer box) of the label a focus style. I like adding the same style that Google Chrome uses because users are most familiar with that style.

/*Adding focus styles on the outer-box of the fake checkbox*/
.checkbox input[type="checkbox"]:focus + label::before {
outline: rgb(59, 153, 252) auto 5px;
Adding the focus style for accessibility

Try accessing the checkbox in the above example with a keyboard.

And we are done! We have a fully accessible, customizable and functional checkbox in just four simple steps.

Don’t forget to check out the pure CSS checkboxes and radio buttons in Project Clarity and feel free to let us know of your feedback!

Clarity Design System

Clarity is an open source design system that brings…

Thanks to Red Dolan and Scott Mathis

Aditya Bhandari

Written by

UI Engineer

Clarity Design System

Clarity is an open source design system that brings together UX guidelines, an HTML/CSS framework, and Angular components. Visit our website here: http://clarity.design

Aditya Bhandari

Written by

UI Engineer

Clarity Design System

Clarity is an open source design system that brings together UX guidelines, an HTML/CSS framework, and Angular components. Visit our website here: http://clarity.design

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store