Pure CSS Styled Checkboxes

Along with <select> tags, styling checkboxes is one of those annoying jobs every front-end dev will have been tasked with at some point.

It’s hardly a Sisyphean task, but while there are many solutions out there many of them rely on adding extraneous markup, vendor prefixes, image assets or javascript. This only compounds the hassle of styling an input that you’ll generally always need.

I thought I’d throw my hat into the ring with my checkbox/radio button styling solution, I think it’s pretty elegant, hopefully someone else will too! This may well be a common technique pioneered long ago so I’ll try and add some value by discussing how each part works in detail.

If you’re an experienced front-end developer and you don’t appreciate being taught how to suck eggs, check out the basic example here.


This solution requires a few modern browser features, if your supported browser list can handle these you should be good to go. If you’re new to CSS, I’d advise reading up on these at some point.

  1. inline-block display elements
  2. CSS Attribute selectors
  3. The :before pseudo-selector.
  4. the :checked pseudo-selector.
  5. Unicode characters


If you’re unlucky enough to have IE7 or lower in your supported browser stack, display-inline, CSS Attribute selectors and the :checked pseudo selector are not supported. Boo!

Honestly, I’d argue that users on legacy IE don’t need pretty checkboxes, especially if it means bloating your stack with a polyfill purely for an aesthetic nicety. Give them default checkbox styles and call it graceful degradation if you need to justify it. If they must be implemented, I’d probably investigate techniques that don’t require CSS3. If you’ve got your heart set on this one, Selectivizr looks like a good bet for improving IE selector support.

The markup

<input id="example-checkbox" type="checkbox">
<label for="example-checkbox"> Example checkbox label</label>

Yep, that’s all you need! What’s important to note is that the <label> MUST directly follow the <input>, otherwise our :checked pseudo-selector won’t work.

Setting up the styles

We don’t need no stinkin’ checkbox! The first thing we’re going to do is hide the checkbox element completely:

input[type="checkbox"] {
display: none;

This will leave you with nothing but a lonely <label>. Thankfully our <label> has been given a for attribute that matches the id of our checkbox. As well as providing a semantic benefit, the for attribute allows users to click on the <label> to ‘focus’ the designated <input>.

With a standard text <input>, this simply places the cursor in the field ready for user input, however with a checkbox it actually checks the box for us. Essentially, clicking the <label> toggles the checked status of our <input>.

Despite our checkbox being hidden, we still have a way of capturing the user input, although currently it’s not clear that the <label> represents a checkbox, let alone the state of the checkbox.

To resolve this, we’re going to use the :before pseudo selector to add an element into the DOM.

input[type="checkbox"] + label:before {
content: '\00a0';
display: inline-block;
width: 20px;
height: 20px;
font-size: 16px;
font-family: 'Arial Unicode MS';
background-color: white;
border: 1px solid grey;
color: #000;
text-align: center;
cursor: pointer;

Lets have a look what’s going on here:

input[type="checkbox"] + label:before

Our selector begins by targeting all <input> tags with the type of checkbox. It then uses the ‘+’ operator to select all <label> tags that directly follow any checkboxes. Finally, it uses the :before pseudo-selector to add a node inside the <label>.

content: '\00a0';

Our first property is a mandatory property for the :before and :after pseudo selectors as it specifies what should be inserted inside the parent node. In this case, we’re inserting a non-breaking space using an escaped unicode character.

display: inline-block;
width: 20px;
height: 20px;
font-size: 16px;
font-family: ‘Arial Unicode MS’;
background-color: white;
border: 1px solid grey;
color: #000;
text-align: center;
cursor: pointer;

These are the styles our custom checkbox will use in it’s unchecked state; this is where you should add/modify styles to suit your design. For simplicities sake I’ve left this example pretty sparse, but I’ve included a more stylish example at the end of this article.

The most important rule here (and the one you should be careful about changing) is the display property. By default, a node inserted using :before or :after is an inline element, which means — among other things — you won’t be able to set a width or height on the element. Setting it to inline-block allows us greater control over the styling of the node, but still keeps it inline with the <label> text.

The font-family rule is also quite important — I’ve used Arial Unicode MS as it supports a wide array of Unicode glyphs and is fairly websafe. This ensures that browsers will render the same glyph consistently.

Recap: You’ve added an <input> tag with a <label> directly following it, and the elements are tied together using the id attribute of the <input> and the for attribute of the <label>. You’ve hidden the <input>, and inserted a node in front of the <label> text using the :before pseudo-selector. You’ve created a fake checkbox by styling the inserted node.

Checking the box

Now we’ve got our unchecked state, we need to implement our checked state:

input[type=”checkbox”]:checked + label:before {
content: '\2714';

We’ve added :checked to the input in our selector. The selector is still eventually targeting the :before node of our label, but these styles will only apply to the node when the preceding checkbox is checked.

We’re also defining the content again, this time with the Unicode character HEAVY CHECK MARK.

Ta da! Your styled checkbox should be a fully functional ugly duckling, awaiting some nicer styles.

Recap: You’ve hidden the real checkbox and inserted a fake checkbox inside the <label>. If a user clicks on a <label> the hidden <input> checkbox will be checked. This triggers the CSS selector that adds a check symbol inside of the fake checkbox.

Further Styling

The example above isn’t particularly pretty, but you can easily take it a lot further. The positioning of each element can be a little tricky as the :before node and the label text are both considered inline with each other. If you try to vertically position your fake checkbox with margin-top, you’ll find your label text moving down too.

I generally use position: relative on the :before node to align the checkmark and the label text and then use line-height on the <label> to center them both against the checkbox background. Here’s an example of a nicer looking checkbox that uses this technique (as well as a different Unicode check symbol):

If you’re still struggling to line them up, try playing with the font-size of the :before node.

You can of course use any unicode symbol for your checked and unchecked state, check out Unicode Table for a comprehensive list of what’s available. If you really want to be fancy, you could create your own Unicode font and import it using @font-face, giving you lots more creative options.

Enhancing with :after

The same technique will work with the :after selector, giving you another label node with a checked and unchecked state. In this particularly obnoxious example I use two characters from the Emoticon unicode character set in the :before node, and the main label text in the :after node. Putting the main text in :after, allows me to switch between two different messages depending on the checked state:

If you ever see that in production, I’m so sorry.

Wrapping Up

That’s it! Let me know any thoughts or comments or if you’ve used this technique in production let me know and I’ll add a link here!