Custom radio and checkbox inputs using CSS
In my never ending quest to find weird and wonderful ways to abuse CSS and all its little intricacies, I have come up with a pretty good way of using CSS to create custom radio and checkbox inputs without JavaScript, that are accessible, keyboard controlled, don’t use any hacks and degrade nicely in non supporting browsers. The journey wasn’t easy and I was on the brink of filing it in the “to crazy” folder, never to be seen again. Luckily I had a brain wave that paid off and actually allowed this to be a very viable solution that degrades beautifully and works in 80% of the browsers. This is my story.
It’s a bug, no it’s intended…well actually it’s still a draft
So upon my initial investigation of how doing something like this would be possible, I came across what I initially thought was a bug with Firefox and IE8. Applying CSS generated content to form elements doesn’t work, turns out Firefox and IE8 are following the CSS3 draft specification as specified in the CSS3 Generated and Replaced Content Module.
Form elements fall under the “replaced elements” category and therefore don’t allow :before
and :after
pseudo-elements on them. However Safari, Chrome and Opera all allow generated content on form elements. Further to the odd behaviour Chrome and Safari will only apply generated content on checkboxes, radios and file inputs whereas Opera will allow it for all form elements, see test case.
You can see my initial attempt at creating custom radios and checkboxes. This applied the generated content directly to the inputs themselves. Open up the demo in Safari or Chrome to see it working. This presented yet another problem this time with Opera, clicking the radio or checkbox directly would never actually check the box whereas with Chrome and Safari the box would be checked and the custom replacement would change state accordingly. But if you clicked the label, which uses the for attribute to create the relationship to the corresponding input, it would check the input in Opera and change the custom check state correctly.
I then thought I would see if I could fix it by using some JavaScript to check the input onclick
, this worked for Opera but stopped it working in Chrome and Safari as it was putting it in an indefinite indeterminate state, as it was checked then quickly unchecked by the JavaScript.
document.addEventListener('click', e => {
var node = evt.target;
if (node.nodeName === "INPUT") {
var isChecked = node.checked;
(isChecked) ? isChecked = false: isChecked = true;
}
});
As there was no safe obvious way that didn’t involve browser sniffing or using bad object detection to determine if the browser behaved in this way. And the fact it was taking away from the whole point of this demo, to create a CSS only solution for custom forms, I scrapped my idea and went back to the drawing board.
Selectors, pseudo-classes & pseudo-elements
As you can see from the demo each radio and checkbox is replaced with a custom one. The difference here from my initial attempt, and to get around the fact you can’t apply generated content to form elements consistently, is to apply the generated content to the label rather than the input itself, and using some clever selectors I can determine the state of the radio/checkbox is in and adjust the custom replaced one accordingly. I also set the original input opacity to 0 so it won’t show through our custom one.
<p>
<input type="radio" value="male" id="male" name="gender" />
<label for="male">Male</label>
</p>
p:not(#foo)>input+label {
background: url(gr_custom-inputs.png) -1px no-repeat;
height: 16px;
padding: 18px;
}
Basically this selector is looking for a label that is immediately preceded by a sibling input, that is a child of a paragraph that doesn’t have an id of foo.
The reason for the not selector is to not apply these styles in IE8, I’ll explain why further down.
content
property, I apply a background and insert 3 non-breaking space characters, since the content property doesn’t accept named entities I used the unicode equivalent of