Styling checkbox and radio button inputs to match a custom design is nearly impossible because neither reliably supports basic CSS, like background colors or images; it's even a challenge to get the margins to appear consistently across browsers. To remedy this we developed a concise jQuery plugin based on progressive enhancement that leverages an input element's built-in functionality and accessibility features and works in all modern browsers without added markup or mandatory CSS classes.
Markup
We start with basic HTML for each input that follows web standards conventions:
- assigned a unique id and value to each input
- paired the input with a label element
- included a "for" attribute on each label that references the preceding input's id
Each radio button input also needs a common name attribute to group it with a set.
< legend >Which genres do you like?</ legend > |
< input type = "checkbox" name = "genre" id = "check-1" value = "action" /> |
< label for = "check-1" >Action / Adventure</ label > |
< legend >Caddyshack is the greatest movie of all time, right?</ legend > |
< input type = "radio" name = "opinions" id = "radio-1" value = "1" /> |
< label for = "radio-1" >Totally</ label > |
Pairing the inputs and labels correctly is essential to how this plugin works. As stated in the HTML spec, "When a LABEL element receives focus, it passes the focus on to its associated control." Browsers have standardized this behavior so that when you click a label, the click is passed on to the input — in other words, the label and input act as a single element when marked up this way. Because we don't have to interact with the input directly, we can hide it from view with CSS and apply styles to the label to make it look like a customized checkbox or radio button.
When the page loads, the plugin script finds each input/label pair and wraps it in a div. Each wrapper div is assigned a class to it based on the type of input it contains:
< div class = "custom-checkbox" > |
< input id = "check-3" type = "checkbox" value = "epic" name = "genre" /> |
< label class = "" for = "check-3" >Epic / Historical</ label > |
Styles
First, we absolutely positioned the input and label pair so that we could layer the label over the input, like a mask. For this to work, we relatively positioned the wrapper div to contain the input and label:
.custom-checkbox, .custom-radio { position : relative ; } |
padding : . 5em 0 . 5em 30px ; |
Next, we styled each type of label (checkbox and radio button) with a background image — we used an image sprite for all states: default, hover, and checked:
background : url (images/checkbox.gif) no-repeat ; |
background : url (images/radiobutton.gif) no-repeat ; |
And added classes for hover and checked states that repositioned the background sprite accordingly. We also included a class for the "focus" state for keyboard users.
.custom-checkbox label, .custom-radio label { |
background-position : -10px -14px ; |
.custom-checkbox label.hover, |
.custom-checkbox label.focus, |
.custom-radio label.hover, |
.custom-radio label.focus { |
background-position : -10px -114px ; |
.custom-checkbox label.checked, |
.custom-radio label.checked { |
background-position : -10px -214px ; |
.custom-checkbox label.checkedHover, |
.custom-checkbox label.checkedFocus { |
background-position : -10px -314px ; |
.custom-checkbox label.focus, |
.custom-radio label.focus { |
outline : 1px dotted #ccc ; |
Script
Because the label-input association takes care of clicking the hidden input for us, we only had to write a really simple jQuery plugin that appends a class to each input on hover, on focus, and on click:
jQuery.fn.customInput = function (){ |
$( this ).each( function (i){ |
if ($( this ).is( '[type=checkbox],[type=radio]' )){ |
var label = $( 'label[for=' +input.attr( 'id' )+ ']' ); |
var inputType = (input.is( '[type=checkbox]' )) ? 'checkbox' : 'radio' ; |
').insertBefore(input).append(input, label); |
var allInputs = $( 'input[name=' +input.attr( 'name' )+ ']' ); |
$( this ).addClass( 'hover' ); |
if (inputType == 'checkbox' && input.is( ':checked' )){ |
$( this ).addClass( 'checkedHover' ); |
function (){ $( this ).removeClass( 'hover checkedHover' ); } |
input.bind( 'updateState' , function (){ |
if (input.is( ':checked' )) { |
if (input.is( ':radio' )) { |
allInputs.each( function (){ |
$( 'label[for=' +$( this ).attr( 'id' )+ ']' ).removeClass( 'checked' ); |
label.addClass( 'checked' ); |
else { label.removeClass( 'checked checkedHover checkedFocus' ); } |
$( this ).trigger( 'updateState' ); |
if (inputType == 'checkbox' && input.is( ':checked' )){ |
$( this ).addClass( 'checkedFocus' ); |
.blur( function (){ label.removeClass( 'focus checkedFocus' ); }); |
Usage
Simply call the customInput()
method on any input element or group of elements (more on using jQuery):
$( 'input' ).customInput(); |