Custom checkbox, radio and on/off switch styling with (S)CSS

Browser's regular checkboxes and radios are often ugly or may just not fit into your design. In this turorial I show you how to apply your own styling for radios and checkboxes using CSS and fontawesome icons for the checkmark. Feel free to change the styling according to your needs (depending on your icon font and your default font you have to do it anyway). In addition I explain how create an on/off switch. I use CSS3, but I provide a fallback to render older browser's default checkbox and radio (IE8). For better code structure I am using SCSS (SASS) in this tutorial, but you can find the compiled CSS at the end.

Checkout the Demo!

Screenshot:

Screenshot of the custom

Checkboxes and radios

I use prefixes for my code so I don't have to worry about overriding third party code. I use fh- in this example, but of course you can use your own. As the styles for checkboxes and radios are pretty much the same, I put them together to avoid code duplication. For better readability I only show the code belonging to the current step and replace the rest of the code with // .... You can find the complete code at the end.

Let's begin! As styling of the default input elements is limited, we will just hide them completely and create our own styling depending on the state of the input.

Lets start with the markup. Please note that the order of the input and label elements is important!

<!-- Checkbox -->
<span class="fh-checkbox">
    <input type="checkbox" id="checkbox-id" name="mycheckbox">
    <label for="checkbox-id">My checkbox label</label>
</span>

<!-- Radio -->
<span class="fh-radio">
    <input type="radio" id="radio-id1" name="myradiogroup">
    <label for="radio-id1">My radio label</label>
</span>
<span class="fh-radio">
    <input type="radio" id="radio-id2" name="myradiogroup">
    <label for="radio-id2">My second radio label</label>
</span>

Now we create a SCSS-file and start with the actual styling. First we define color and margin variables at the top of the file to make changing the look of our elements easier:

$color-default: #849FBB; // default color
$color-default-light: #DDDDDD; // default color light
$color-active: #57CB85; // color when active or checked
$color-active-light: #9EFFC4; // active color light
$color-focus: #6FB5FB; // color when focused

$margin-el: 7px; // default margin for our custom inputs

To apply the custom styling only to browsers which support it, we use the CSS selectors :checked and :not(:checked). This will cause IE8 to ignore our code and to render the default checkbox and radio:

.fh-checkbox > [type="checkbox"],
.fh-radio > [type="radio"] {
    &:checked,
    &:not(:checked) {
       // here goes our styling
    }
}

One important thing we have to do is to hide the default input element because changing its style is limited. We do that by setting opacity: 0; display: none;, as only using display: none can cause the elements to still show in some browsers:

.fh-checkbox > [type="checkbox"],
.fh-radio > [type="radio"] {
    &:checked,
    &:not(:checked) {
        width: 0;
        height: 0;
        display: none;
        opacity: 0;
    }
}

Now we add some margins and paddings to the label-element to make room for the checkboxes and radios:

.fh-checkbox > [type="checkbox"],
.fh-radio > [type="radio"] {
    &:checked,
    &:not(:checked) {
        // ...
        & + label {
            display: inline-block;
            margin-right: $margin-el;
            margin-top: $margin-el;
            margin-bottom: $margin-el;
            padding-left: 22px;
            padding-top: 2px;
            position: relative;
            cursor: pointer;
        }
    }
}

To add the base styling for the new radios and checkboxes, we use the :before-pseudoelement of the label:

.fh-checkbox > [type="checkbox"],
.fh-radio > [type="radio"] {
    &:checked,
    &:not(:checked) {
        // ...
        & + label {
            // ...
            &:before {
                // position elements absolute to parent container
                position: absolute;
                display: inline-block;
                bottom: 1px;
                left: 0;
                width: 13px;
                height: 13px;
                border: 2px solid $color-default;
                color: $color-default;
            }
            &:hover:before {
                // add some hover styling
                border-color: $color-default;
            }
        }
    }
}

Next off we style the focus-state:

.fh-checkbox > [type="checkbox"],
.fh-radio > [type="radio"] {
    &:checked,
    &:not(:checked) {
        // ...
        &:focus + label:before {
            border-color: $color-focus;
            box-shadow: 0 0 6px 0 $color-focus;
        }
    }
}

Now we style the disabled-state by adding some opacity and a not-allowed cursor:

.fh-checkbox > [type="checkbox"],
.fh-radio > [type="radio"] {
    &:checked,
    &:not(:checked) {
        // ...
        &[disabled] + label {
            cursor: not-allowed;
            opacity: .4;
            &:before {
                opacity: .7;
            }
        }
    }
}

Now we start with the actual styling. Lets begin with the checkbox. By using the pseudoclasses :selected and :not(:selected) we can change our label styling depending on the checked state of our hidden input element (it's :selected state is still changed by clicking on our label). As we have to differ between [type="checkbox"] and [type="radio"] we put them in separate blocks outside of our base code. We also have to use both :checked and :not(:checked) selectors for the default variants:

// styling for checkbox for both states
.fh-checkbox > [type="checkbox"]:checked + label:before,
.fh-checkbox > [type="checkbox"]:not(:checked) + label:before {
    // set icon font
    font-family: "FontAwesome", sans-serif;
    font-size: 13px;
    text-align: center;
    // add some CSS3-animations
    -webkit-transition: border-color .2s ease-in, background-color .2s ease-in;
    -moz-transition: border-color .2s ease-in, background-color .2s ease-in;
    -o-transition: border-color .2s ease-in, background-color .2s ease-in;
    -ms-transition: border-color .2s ease-in, background-color .2s ease-in;
    transition: border-color .2s ease-in, background-color .2s ease-in;        
}
// styling for checkbox when selected
.fh-checkbox > [type="checkbox"]:checked + label:before {
    // set checkmark icon
    content: "\f00c";
    color: #FFF;
    background-color: $color-active;
    border-color: $color-active;
}
// styling for checkbox when not selected
.fh-checkbox > [type="checkbox"]:not(:checked) + label:before {
    // remove checkmark icon
    content: "";
} 

Now we do the same for the radio:

// styling for radio for both states
.fh-radio > [type="radio"]:checked + label:before,
.fh-radio > [type="radio"]:not(:checked) + label:before, {
    content: "";
    border-radius: 15px;
    // add some CSS3-animations
    -webkit-transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in, box-shadow .2s ease-in;
    -moz-transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in;
    -o-transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in;
    -ms-transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in;
    transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in;
} 
// styling for radio when selected
.fh-radio > [type="radio"]:checked + label:before {
    color: $color-active;
    border-color: $color-active;
    background-color: $color-active;
    // use box-shadow to render circle
    box-shadow: inset 0 0 0 2px #fff;  
}  
// styling for radio when not selected
.fh-radio > [type="radio"]:not(:checked) + label:before {
    box-shadow: inset 0 0 0 3px #fff; 
}

And voilĂ , you got your custom styled radios and checkboxes!

On/off switch

Now let's start with the on/off switch. Its styling is quite similar to the radios and checkboxes. We will use a regular checkbox inside a wrapper span. The wrapper contains another span element which represents our switch knob.

The markup:

<span class="fh-switch">
   <input type="checkbox" id="switch-id" name="myswitch">
   <label for="switch-id">My switch label</label>
   <span class="fh-switch-knob"></span>
</span>

To use absolute positioning we have to set the wrapper relative:

.fh-switch {
    position: relative;
}

Now we hide the default checkbox and style our label, as we did before:

.fh-switch > [type="checkbox"]:checked,
.fh-switch > [type="checkbox"]:not(:checked) {
    width: 0;
    height: 0;
    display: none;
    opacity: 0;
    & + label {
        cursor: pointer;
        display: inline-block;
        margin-right: $margin-el;
        margin-top: $margin-el;
        margin-bottom: $margin-el;
        // apply padding so the switch fits inside the label
        padding-right: 60px;
    }
}

Next we style the background bar of the switch via the :after element of the label:

.fh-switch > [type="checkbox"]:checked,
.fh-switch > [type="checkbox"]:not(:checked) {
    //...
    & + label {
        //...
        &:after {
            content: "";
            top: 4px;
            right: 10px;
            width: 30px;
            height: 12px;
            // use absolute for better positioning
            position: absolute;
            border-radius: 30px;
        }
    }
}

Now we add the base styling for the switch knob:

.fh-switch > [type="checkbox"]:checked,
.fh-switch > [type="checkbox"]:not(:checked) {
    //...
    & + label {
        //...
        &+ .fh-switch-knob {
            top: 0;
            width: 20px;
            height: 20px;
            border-radius: 30px;
            display: inline-block;
            position: absolute;
            cursor: pointer;
            pointer-events: none;
            box-shadow: 1px 1px 1px $color-default-light;
            // add some CSS3-animations
            -webkit-transition: right .1s ease-in, background-color .1s ease-in;
            -moz-transition: right .1s ease-in, background-color .1s ease-in;
            -o-transition: right .1s ease-in, background-color .1s ease-in;
            -ms-transition: right .1s ease-in, background-color .1s ease-in;
        }
    }
}

Here we define the appearence of the switch bar depending on the checked state:

.fh-switch > [type="checkbox"]:checked,
.fh-switch > [type="checkbox"]:not(:checked) {
    //...
    &:checked + label:after {
        background-color: $color-active-light;
    }
    &:not(:checked) + label:after {
        background-color: $color-default-light;
    }
}

And we do the same for the switch-knob:

.fh-switch > [type="checkbox"]:checked,
.fh-switch > [type="checkbox"]:not(:checked) {
    //...
    &:checked + label + .fh-switch-knob {
        right:5px;
        background-color: $color-active;
    }
    &:not(:checked) + label + .fh-switch-knob {
        right: 25px;
        background-color: $color-default;
    }
}

Now only the :focus and disabled states are missing:

.fh-switch > [type="checkbox"]:checked,
.fh-switch > [type="checkbox"]:not(:checked) {
    //...
    &:focus + label:after,
    &:focus + label + .fh-switch-knob {
        box-shadow: 0 0 6px 0 $color-focus;
    }
    &[disabled] {
        & + label,
        & + label:after,
        & + label + .fh-switch-knob {
            cursor: not-allowed;
            opacity: 0.4;
        }
    }
}

And hurray, we are finished! :) The whole file containing radios, checkboxes and the switch should look like this now:

$color-default: #849FBB; // default color
$color-default-light: #DDDDDD; // default color light
$color-active: #57CB85; // color when active or checked
$color-active-light: #9EFFC4; // active color light
$color-focus: #6FB5FB; // color when focused

$margin-el: 7px; // default margin for our custom inputs

/**
 * Checkboxes & radios
 */
.fh-checkbox > [type="checkbox"],
.fh-radio > [type="radio"] {
    &:checked,
    &:not(:checked) {
        width: 0;
        height: 0;
        display: none;
        opacity: 0;
        & + label {
            display: inline-block;
            margin-right: $margin-el;
            margin-top: $margin-el;
            margin-bottom: $margin-el;
            padding-left: 22px;
            padding-top: 2px;
            position: relative;
            cursor: pointer;
            &:before {
                // position elements absolute to parent container
                position: absolute;
                display: inline-block;
                bottom: 1px;
                left: 0;
                width: 13px;
                height: 13px;
                border: 2px solid $color-default;
                color: $color-default;
            }
            &:hover:before {
                // add some hover styling
                background-color: $color-default;
            }
        }
        &:focus + label:before {
            border-color: $color-focus;
            box-shadow: 0 0 6px 0 $color-focus;
        }
        &[disabled] + label {
            cursor: not-allowed;
            opacity: .4;
            &:before {
                opacity: .7;
            }
        }
    }
}
// styling for checkbox for both states
.fh-checkbox > [type="checkbox"]:checked + label:before,
.fh-checkbox > [type="checkbox"]:not(:checked) + label:before {
    // set icon font
    font-family: "FontAwesome";
    font-size: 13px;
    text-align: center;
    // add some CSS3-animations
    -webkit-transition: border-color .2s ease-in, background-color .2s ease-in;
    -moz-transition: border-color .2s ease-in, background-color .2s ease-in;
    -o-transition: border-color .2s ease-in, background-color .2s ease-in;
    -ms-transition: border-color .2s ease-in, background-color .2s ease-in;
    transition: border-color .2s ease-in, background-color .2s ease-in;        
}
// styling for checkbox when selected
.fh-checkbox > [type="checkbox"]:checked + label:before {
    // set checkmark icon
    content: "\f00c";
    color: #FFF;
    background-color: $color-active;
    border-color: $color-active;
}
// styling for checkbox when not selected
.fh-checkbox > [type="checkbox"]:not(:checked) + label:before {
    // remove checkmark icon
    content: "";
}
// styling for radio for both states
.fh-radio > [type="radio"]:checked + label:before,
.fh-radio > [type="radio"]:not(:checked) + label:before, {
    content: "";
    border-radius: 15px;
    // add some CSS3-animations
    -webkit-transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in, box-shadow .2s ease-in;
    -moz-transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in;
    -o-transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in;
    -ms-transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in;
    transition: border-color .2s ease-in, box-shadow .1s ease-in, background-color .2s ease-in;
} 
// styling for radio when selected
.fh-radio > [type="radio"]:checked + label:before {
    color: $color-active;
    border-color: $color-active;
    background-color: $color-active;
    // use box-shadow to render circle
    box-shadow: inset 0 0 0 1px #fff;  
}  
// styling for radio when not selected
.fh-radio > [type="radio"]:not(:checked) + label:before {
    box-shadow: inset 0 0 0 3px #fff; 
}
/**
 * On/off switch
 */
.fh-switch {
    position: relative;
}
.fh-switch > [type="checkbox"]:checked,
.fh-switch > [type="checkbox"]:not(:checked) {
    width: 0;
    height: 0;
    display: none;
    opacity: 0;
    & + label {
        cursor: pointer;
        display: inline-block;
        margin-right: $margin-el;
        margin-top: $margin-el;
        margin-bottom: $margin-el;
        // apply padding so the switch fits inside the label
        padding-right: 60px;
        &:after {
            content: "";
            top: 4px;
            right: 10px;
            width: 30px;
            height: 12px;
            // use absolute for better positioning
            position: absolute;
            border-radius: 30px;
        }
        & + .fh-switch-knob {
            top: 0;
            width: 20px;
            height: 20px;
            border-radius: 30px;
            display: inline-block;
            position: absolute;
            cursor: pointer;
            pointer-events: none;
            box-shadow: 1px 1px 1px $color-default-light;
            // add some CSS3-animations
            -webkit-transition: right .1s ease-in, background-color .1s ease-in;
            -moz-transition: right .1s ease-in, background-color .1s ease-in;
            -o-transition: right .1s ease-in, background-color .1s ease-in;
            -ms-transition: right .1s ease-in, background-color .1s ease-in;
        }
    }
    &:focus + label:after,
    &:focus + label + .fh-switch-knob {
        box-shadow: 0 0 6px 0 $color-focus;
    }
    &[disabled] {
        & + label,
        & + label:after,
        & + label + .fh-switch-knob {
            cursor: not-allowed;
            opacity: 0.4;
        }
    }
}
.fh-switch > [type="checkbox"]:checked + label:after {
    background-color: $color-active-light;
}
.fh-switch > [type="checkbox"]:not(:checked) + label:after {
    background-color: $color-default-light;
}
.fh-switch > [type="checkbox"]:checked + label + .fh-switch-knob {
    right:5px;
    background-color: $color-active;
}
.fh-switch > [type="checkbox"]:not(:checked) + label + .fh-switch-knob {
    right: 25px;
    background-color: $color-default;
}

Note that the code can be optimized, i.e. you could put the different &:not(:checked) + label + ... and the &:checked + label + ... of the on/off switch together, but I wrote it like this for better understanding. And of course there are still some other things left you have to do, for example increase the size of the inputs on mobile devices and so on. But this tutorial should give you a good start.

If you encounter any errors in this tutorial or if you think something is not understandable, or if you see something which could be done better otherwise, please create a pull-request on github or contact me!

View SCSS file

View CSS file

Visit github

Go back to overview