Complete guide on how to style checkbox element with css

Why style checkbox?

Let's face it - default checkbox appearance is not that great on all browsers. Often you want to have unique style and match all UI elements to your brand color and design scheme. Styling checkbox elements becomes a natural thing that you have to do. However, we can't directly style <input type="checkbox"/> html element. There are several ways we could tackle this problem: with javascript, with background images or with simple plain css. In this guide you will learn how to style checkbox with only css. One solution is to use css pseudo elements and style a <span> inside <label> and make <input type="checkbox"/> hidden. This will give you full control of checkbox styles and will make your html code semantically correct with correct and working accessibility features.

Foundations of checkbox structure

Let's start with our input and its label elements and add some span elements inside the label

<input type="checkbox" id="checkbox-element" />
<label for="checkbox-element">
    <span class="indicator"></span>
    <span class="label-text">Old fashion of showing checkbox</span>
</label>

This is how our checkbox will look like:

Nothing fancy yet. The looks of checkbox will differ on different operating systems and browsers. However, we now have a <span class="indicator"> element which we will later use for styling and making our artificial checkbox element.

But first let's hide the native input element, so it does not interfere with our styled checkbox. We can do that and still make everything work, because we linked label with for attribute to id of checkbox input and when we click on label the state of checkbox will be toggled. We can hide input by setting its opacity to 0 or setting position value absolute and setting any of the positioning values (top, bottom, left, right) to an offscreen value.

input[type=checkbox]{
    position:absolute;
    opacity: 0;
    z-index: -9999;
}

Now you will see only the label text. And this is exactly how we want this to work at the moment.

CSS pseudo elements ::before and ::after

We are now ready to move on to the actual styling of <span class="inidcator"> element. We will be using ::before pseudo element to style the checkmark of our checkbox. What pseudo elements do is essentially insert blank html element before or after our current element - in this case <span class="inidcator"> without actually inserting a real DOM element. You will often see ::before or ::after pseudo elements used whenever there is a need to create more complicated css effects or styles.

Note that we are using double colon :: instead of single colon : when specifying ::before and ::after. Most of the modern browsers support double colon syntax, and it is the recommended way of specifying pseudo elements. Single colon is used for specifying pseudo selectors like :first-child.

Let's extend our css and create the box for our styled checkbox. We will be using adjacent sibling combinator aka the + css selector to select the first label element which comes directly after our checkbox input element. By doing this we will make sure we are selecting the correct element for specific input element and things don't get crazy when we have multiple checkbox elements next to each other under the same parent container. So we should make sure that our input element is always first and the label comes after it. Then we will style the <span class="indicator"> element to create the box of checkbox. CSS pseudo elements ::before or ::after for <span class="indicator"> can be used to create the checkmark sign.

Firstly we want to set our label's position to relative as we will be making the indicator absolute and we don't want to have indicator fly away from the label's bounding box, also to make everything accordingly spaced we will add padding-left for the label and set it to be similar to the size of our indicator. This will make sure our checkbox indicator is not overlapping with the label text.

input[type=checkbox] + label{
    position: relative;
    padding-left: 24px;
}

Then we style the .indicator span and make it look like a small square checkbox box. It is important to make its position absolute and give it some negative left positioning, so the box looks nicely separated from the label text. We should also make the background color differ for :checked state, so it is distinguishable for the user what state the checkbox currently has.

input[type=checkbox] + label .indicator{
    position: absolute;
    left: -4px;
    display: block;
    width: 24px;
    height: 24px;
    background: #fff;
    border-radius:3px;
    border:1px solid #ddd;
    cursor: pointer;
}

input[type=checkbox]:checked + label .indicator{
    background: #a123cb;
}

The indicator ::after pseudo element will be our checkmark. Let's make it positioned absolute and of rectangle shape, around two-thirds of the indicator checkbox size. There is no big difference whether you use ::after or ::before as we set the pseudo element to be positioned absolute. Removing 2 of the 4 borders and rotating rectangle by 45 degrees gives us a nice checkmark. Depending on selected sizes checkmark will most probably be off-centered, so we can center it by pushing it a few pixels to the left.

input[type=checkbox]:checked + label .indicator::after{
    content: "";
    position: absolute;
    width: 10px;
    height: 16px;
    border: 4px solid #fff;
    border-left: none;
    border-top: none;
    transform: rotate(45deg);
    left: 6px;
}

Final css looks like this:

input[type=checkbox]{
    position:absolute;
    opacity: 0;
    z-index: -9999;
}

input[type=checkbox] + label{
    position: relative;
    padding-left: 24px;
}

input[type=checkbox] + label .indicator{
    position: absolute;
    left: -4px;
    display: block;
    width: 24px;
    height: 24px;
    background: #fff;
    border-radius:3px;
    border:1px solid #ddd;
    cursor: pointer;
}

input[type=checkbox]:checked + label .indicator{
    background: #a123cb;
}

input[type=checkbox]:checked + label .indicator::after{
    content: "";
    position: absolute;
    width: 10px;
    height: 16px;
    border: 4px solid #fff;
    border-left: none;
    border-top: none;
    transform: rotate(45deg);
    left: 6px;
}

We now have custom styled checkbox element which has different styles based on the state of underlying checkbox input:

More visual interaction

We now have fully functional css styled checkbox element. However, we could still improve it by adding different interaction based on mouse event like :hover.

We could change the background of .indicator when user hovers over checkbox when checkbox is unchecked. This will give small but meaningful visual interaction.

input[type=checkbox] + label .indicator:hover{
    background: #ccc;
}

Don't forget the :focus state

It also important to remember that when we click on the custom checkbox we set our underlying <input type="checkbox"> to :focus state. We need to make sure that we add some visual interaction when the checkbox is focused. We can achieve this by adding some styles to our .indicator span when our checkbox input is in :focus state. This will also allow our keyboard users to use tab button to select the checkbox and know which checkbox is currently selected.

input[type=checkbox]:focus + label .indicator{
    box-shadow: 0px 0px 1px 3px #ca2bff;
}

Lastly we could add some transitions to make everything look smoother instead of instantly changing colors.

input[type=checkbox] + label .indicator{
    position: absolute;
    left: -4px;
    display: block;
    width: 24px;
    height: 24px;
    background: #fff;
    border-radius:3px;
    border:1px solid #ddd;
    cursor: pointer;
    transition: box-shadow 200ms, border 200ms, background 200ms;
}

Final result

This is how our final checkbox css html and visuals look like. This could be extended to have more style features, better and more complex transition or checkmark animations.

<style>
    input[type=checkbox]{
        position:absolute;
        opacity: 0;
        z-index: -9999;
    }
    input[type=checkbox] + label{
        position: relative;
        padding-left: 24px;
    }
    input[type=checkbox] + label .indicator{
        position: absolute;
        left: -4px;
        display: block;
        width: 24px;
        height: 24px;
        background: #fff;
        border-radius:3px;
        border:1px solid #ddd;
        cursor: pointer;
        transition: box-shadow 200ms, border 200ms, background 200ms;
    }
    input[type=checkbox] + label .indicator:hover{
        background: #ccc;
    }
    input[type=checkbox]:focus + label .indicator{
        box-shadow: 0px 0px 1px 3px #ca2bff;
    }
    input[type=checkbox]:checked + label .indicator{
        background: #a123cb;
    }
    input[type=checkbox]:checked + label .indicator::after{
        content: "";
        position: absolute;
        width: 10px;
        height: 16px;
        border: 4px solid #fff;
        border-left: none;
        border-top: none;
        transform: rotate(45deg);
        left: 6px;
    }
    
    /** Background variations **/
    input.blue[type=checkbox]:checked + label .indicator{
        background: #23a4cb;
    }
    input.green[type=checkbox]:checked + label .indicator{
        background: #29cb23;
    }
</style>
<div>
    <input type="checkbox" id="checkbox-element-1" />
    <label for="checkbox-element-1">
        <span class="indicator"></span>
        <span class="label-text">New styled checkbox</span>
    </label>
    <input type="checkbox" id="checkbox-element-2" class="blue" />
    <label for="checkbox-element-2">
        <span class="indicator"></span>
        <span class="label-text">New styled checkbox 2</span>
    </label>
    <input type="checkbox" id="checkbox-element-3" class="green" />
    <label for="checkbox-element-3">
        <span class="indicator"></span>
        <span class="label-text">New styled checkbox 2</span>
    </label>
</div>