Accessible custom checkboxes and radio buttons

Every now and then I’m handed a design comp that has customised checkboxes and radio buttons. This used to make me think “Oh no, not again” because I simply didn’t know of a reliable way to customise these particular form controls.

Sure, if all you care about is replacing the browser default with a custom graphic it isn’t that hard. But if like me you’re also interested in doing so without degrading user experience, especially during keyboard interaction, you have a number of problems to deal with.

Fortunately the situation is a lot better now than it was a few years ago. My first attempts at custom checkboxes and radio buttons involved quite a bit of JavaScript trickery to toggle between different states of the buttons, and I never got it to work perfectly cross-browser, cross-input device. However, since recent versions of all major browsers support the :checked CSS pseudo-class, you can now leave it to the browser to handle the states and focus on the CSS. No JavaScript involved (except to make it more robust, see below).

This has been done before, and I’ve seen a couple of variations on this technique. The way I do it differs a little so I thought a writeup may be of interest.

The only “advanced” part of this technique is the use of the :checked pseudo-class. It’s been supported for years in most browsers, but IE didn’t get support until version 9. Since that’s also the version that gave IE media query support I’ve wrapped the CSS in a media query to hide it from older browsers (which simply get their normal checkboxes and radio buttons). Yes, it’s a bit unfair to semi-old versions of the other browsers that do support :checked but not media queries, but it keeps the CSS simpler and they just get their normal checkboxes. Progressive enhancement.


I almost always use a div to enclose form controls and their label element, like this:

<div class="radio">
	<input type="radio" id="radio1" name="radio1" />
	<label for="radio1">A radio button</label>

I’d better explain why. Some people like putting each form control inside its label element, but I’ve never felt comfortable with that. It’s valid HTML, sure, but it seems a bit odd to me. Besides, using the for and id attributes to associate labels and form controls has wider compatibility, and keeping the elements separate like this increases styling flexibility. YMMV.


Here’s the CSS I use on the Custom checkboxes and radio buttons demo page:

.radio {
	/* Enable absolute positioning of the hidden form controls */
	/* Just a bit of space. */
	Match line-height to the height of the replacement image to ensure it
	doesn't get clipped
fieldset :last-child {
Position and hide the real checkboxes and radio buttons.
The inputs are made transparent instead of completely hidden to preserve
clickability in browsers that don't have clickable labels, like Safari for
iOS 5 and older.
input[type="radio"] {
	/* Match the image dimensions */
	/* Reset anything that could peek out or interfere with dimensions */
Insert a pseudo element inside each label and give it a background
image that will become the custom checkbox or radio button.
Using inline-block lets you use vertical-align to adjust it vertically
as needed.
input[type="checkbox"] + label:before,
input[type="radio"] + label:before {
	background:url(radio-checkbox.png) no-repeat;
	content:" ";
Position the background image differently depending on the state of each
checkbox and radio button.
input[type="radio"]:focus + label:before {
	background-position:0 -22px;
input[type="radio"]:checked + label:before {
	background-position:0 -44px;
input[type="radio"]:checked:focus + label:before {
	background-position:-0 -66px;
input[type="checkbox"] + label:before {
	background-position:0 -88px;
input[type="checkbox"]:focus + label:before {
	background-position:0 -110px;
input[type="checkbox"]:checked + label:before {
	background-position:0 -132px;
input[type="checkbox"]:checked:focus + label:before {
	background-position:0 -154px;

The comments explain what’s going on. The only “trick” is the use of opacity:0 on the original inputs to ensure that you can still click/tap them in browsers that don’t make labels clickable (like Safari for iOS 5.1 and older).

If you want visual feedback when mouse users hover over the elements, just duplicate the :focus selectors and replace focus with hover. I didn’t include it here to save space.

Known issues

One issue I’m aware of is that Opera Mini doesn’t want to make the original input elements transparent, so both the original and the replaced will be visible. It still works though, so it’s mostly a visual bug.

Another problem is if images are disabled in the browser, or if the image containing the checkboxes and radio buttons fails to load for some reason. If that happens, only the label is shown, which of course is likely to be pretty confusing to sighted users. To reduce the risk of that happening you can use JavaScript to check if images are enabled, and scope the selectors like this:

.images-on input[type="checkbox"],
.images-on input[type="radio"] {
	/* Same declarations as in the previous example */
.images-on input[type="checkbox"] + label:before,
.images-on input[type="radio"] + label:before {
	/* Same declarations as in the previous example */

That way you get the normal checkboxes and radio buttons instead of nothing if images are off.

Of course, if you do not use any images to create the custom elements, the problem goes away. See the clever technique Manuel Strehl describes in On Replacing Checkboxes and Radio Buttons with CSS3 and w/o Images for an example. With this technique the checkbox is created entirely with CSS and does not fail when images are off. Depending on your design requirements this could work in many cases.

Then there is also a problem with Windows high contrast mode, which disables background images and thus hides the custom checkboxes and radio buttons. Steve Faulkner describes how you can check for that in Detecting if images are disabled in browsers.

Just because you can…

While this technique feels much more reliable than the hacky JavaScript solutions that were the only option a few years ago, there are some issues you need to be aware of. As long as those are handled, to the best of my knowledge it works for both keyboard, mouse and touch users.

Oh, and don’t go overboard with this, please. Just because you can doesn’t mean you should.

Posted on November 16, 2012 in CSS, Accessibility