On Replacing Checkboxes and Radio Buttons with CSS3 and w/o Images

Demo


Several people have presented methods to replace the default checkboxes in HTML forms by ones that are more consistent in a given design. While generally this might be unnecessary and even leading to a negative user experience, if these replacements are not being crafted carefully, in some cases it really makes sense. Literally any of those techniques make use of images to replace the native boxes. A good technique has been shown on Filament Group’s blog, that has also the advantage of being accessible by both non-visual agents and agents with trouble downloading the replacement images.

The new properties in CSS 3 however allow sophisticated effects, that come close to what is usually displayed in these replacement images. Hence the logical consequence is to try and fiond a way to replace checkboxes by means of CSS alone.

Apart from that there is a wrapper element needed in the above mentioned aproach to fix the accessibility issue. This element should be avoided.

The following approach incorporates both of these enhancements. It works as advertised in all newer browsers and degrades gracefully in most browsers not capable of CSS beyond version 1. We use simple markup:

<p>
<input type="checkbox" name="foo" value="bar" id="foo">
<label for="foo">This is a label</label>
</p>

The CSS properties for the input element and its label are simple and straight-forward:

input[type="checkbox"] {
margin: 0;
padding: 0;
width: 16px;
}
label {
position: relative;
}

Note, that the label gets the position set to relative. The more intriguing part is the label:before pseudo-element.

label:before {
position: absolute;
left: -21px;
top: -1px;
display: inline-block;
width: 10px;
height: 10px;
padding: 2px 2px 4px 4px;
background: #e5e5e5;
content: " ";
cursor: pointer;
border: 1px solid #999999;
text-align: center;
line-height: 10px;
vertical-align: top;
color: #555;
}

This renders a block, that lies exactly on top of the native checkbox. The key is the absolute positioning and the size of the block, that covers the native box. We give it a basic styling and set its content to be an empty string.

To display the check-hook, we use again the content property. Unfortunately we cannot address our checkbox for the checked case alone, so we have to use Javascript to handle this case. For the sake of brevity, the shown solution needs jQuery, but it can be implemented in plain JS with ease.

$('input:checkbox').change(function() {
// this runs, when the checkbox is changed

// the label, identified by its "for" attribute
var label = $('label[for="'+$(this).attr('id')+'"]');

// add or remove a class "checked" on the label
if ($(this).filter(":checked").length) {
label.addClass("checked");
} else {
label.removeClass("checked");
}
}).trigger("change"); // run the above once on loading to
// get initial values right

We can then address the label belonging to a checked checkbox with the .checked class:

label.checked:before {
content: "\2713";
}

The content is now the Unicode character U+2713, a nice hook.

We’re not ready yet. There is a subtle hover effect in native checkboxes. Furthermore we have to take care of disabled checkboxes, which need a visual feedback.

label:hover:before {
box-shadow: inset 0 0 2px 1px #fd2;
}
label.disabled::before {
border-color: #bbb;
color: #bbb;
text-shadow: 0 -1px #ccc, 0 1px #666;
background: -moz-linear-gradient(top, #d5d5d5, #e5e5e5);
}

Hovering over the label results in an inset orange glimmer, similar to the native Windows version. For deeper browser support changing the border color would be a more solid choice. In the disabled case, the fake checkbox gets lighter colors.

Finally we add a bit of CSS 3 polish onto the new checkbox.

label:before {
background: #e5e5e5;
background: -webkit-linear-gradient(top, #c5c5c5, #e5e5e5);
background: -moz-linear-gradient(top, #c5c5c5, #e5e5e5);
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
box-shadow: inset 0 0 1px #fff;
text-shadow: 0 -1px #fff, 0 1px #000;
}

The finished result is both flexible and accessible, and it needs no extra markup nor a single image. Of course, quite the same approach can be taken for radio buttons, where increasing the border radius allows circle-formed controls.


Update

On reading Lea Verou’s Fronteer ’11 slides and subsequent study of Ryan Seddon’s solution, you can in many browsers now implement a solution for the given markup completely without JavaScript. We utilize for this the :checked and :disabled pseudo-selectors:

input:checked + label:before {
content: "\2713";
}

input:disabled + label:before {
border-color: #bbb;
color: #bbb;
text-shadow: 0 -1px #ccc, 0 1px #666;
background: -moz-linear-gradient(top, #d5d5d5, #e5e5e5);
}