Replacing Radio Buttons Without Replacing Radio Buttons
Forms elements! They’re a pain to style, aren’t they? It’s tempting to replace them altogether, with some custom markup and CSS of our own design. The trouble is, the resultant rat’s nest of div
s and span
s will lack the semantic and behavioral qualities that made the standard type="radio"
input accessible.
<div class="radio-label">
<div class="radio-input" data-checked="false" data-value="accessible"></div>
accessibility?
</div>
This is just a lonely piece of text that says “accessibility?” Tragic, really. To make this even begin to work correctly again, we need to add all sorts of remedial WAI-ARIA semantics. In the immortal words of Iron Maiden, “can I play with madness?”
<div class="radio-label" id="accessible-radio">
<div class="radio-input" data-checked="false" data-value="accessible"
aria-labelledby="accessible-radio" role="checkbox" aria-checked="false">
</div>
accessibility?
</div>
Our example is still one hundred percent inaccessible because we have yet to cludge all of the conventional behaviors and key bindings established by the standard type="radio"
. This will require the tabindex
attribute and JavaScript galore — and do you know what? I’m not even going to begin down that road.
What I have done is available as a CodePen demo, and to follow is an explanation of the technique.
See the Pen Replacing Radio Buttons by SitePoint (@SitePoint) on CodePen.
Note: If you’ve not used radio buttons with a keyboard before, know that you are able to focus the active button using the TAB key and change the active button using the UP and DOWN arrow keys. This is standard UA behavior, not a JavaScript emulation.
Use what’s already there
To think accessibly, you need to consider the HTML the interface and the CSS merely the appearance of that interface; the branding. Accordingly, we need to look for ways to seize control of UI aesthetics without relying on the recreation of the underlying markup that marks a departure from standards.
What do we know about radio buttons?
One thing we know about radio buttons is that they can be in either a checked or unchecked state. Never mind ARIA, this is just HTML’s checked
attribute.
<label for="accessible">
<input type="radio" value="accessible" name="quality" id="accessible"> accessible
</label>
<label for="pretty">
<input type="radio" value="pretty" name="quality" id="pretty"> pretty
</label>
<label for="accessible-and-pretty">
<input type="radio" value="pretty"
name="quality" id="accessible-and-pretty" checked> accessible and pretty
</label>
Fortuitously, we can express the checked state via the :checked
pseudo-class in CSS:
[type="radio"]:checked {
/* styles here */
}
Less fortuitously, there aren’t many properties we can place in this block that will actually be honored — especially not consistently across browsers. Radio buttons obstinately refuse to be bent to our will.
The adjacent sibling combinator
I love the adjacent sibling combinator with a passion that a man perhaps should not reserve for CSS selector expressions. It allows me to style elements according to the nature of the elements that precede them.
This is a powerful notion in regard to our radio buttons because it allows us to defer the appearance of state changes onto elements that can actually be styled easily.
[type="radio"]:checked + span {
/* styles for a span proceeded by a checked radio button */
}
We will, of course, have to add span
elements to the markup, but worse fates could befall the HTML.
<fieldset>
<legend>Radio Control Quality</legend>
<label for="accessible">
<input type="radio" value="accessible" name="quality" id="accessible">
<span>accessible</span>
</label>
<label for="pretty">
<input type="radio" value="pretty" name="quality" id="pretty">
<span>pretty</span>
</label>
<label for="accessible-and-pretty">
<input type="radio" value="pretty" name="quality" id="accessible-and-pretty" checked>
<span>accessible and pretty</span>
</label>
</fieldset>
We don’t want to actually style the label text, but we have created the necessary relationship to move visual feedback away from the <input>
. The radio button styling will, in fact, be deferred to the <span>
element’s ::before
pseudo-content.
Hiding the radio button is just a case of employing an accessible hiding technique like that found in HTML5 Boilerplate’s CSS:
[type="radio"] {
border: 0;
clip: rect(0 0 0 0);
height: 1px; margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
But if it’s hidden, how can anyone click it? By nesting the radio button in a <label>
, user agents make the <label>
itself a handler for toggling the radio. This is a good technique in any case because it increases the “hit area” of an otherwise diminutive control.
The styling
As previously mentioned, we will be using pseudo content to forge our “radio button”. This way, we can treat the styling of the label text separately.
[type="radio"] + span::before {
content: '';
display: inline-block;
width: 1em;
height: 1em;
vertical-align: -0.25em;
border-radius: 1em;
border: 0.125em solid #fff;
box-shadow: 0 0 0 0.15em #000;
margin-right: 0.75em;
transition: 0.5s ease all;
}
Note the use of border
and box-shadow
to create the concentric rings. The checked style subsequently transitions the box shadow’s radius spread and incorporates a green on/correct/selected/positive color; the kind that’s usually defined somewhere in your Sass variables.
[type="radio"]:checked + span::before {
background: green;
box-shadow: 0 0 0 0.25em #000;
}
Never forget
All that remains is to incorporate a focus style so that keyboard users can see which element is in their control. An outline
on thin dotted
on the <span>
would suffice, but I have opted for a unicode arrow, pointing to the control via ::after
. This visual feedback is more emphatic than browser vendors provide by default, helping to increase the accessibility of the focus state.
[type="radio"]:focus + span::after {
content: '\0020\2190';
font-size: 1.5em;
line-height: 1;
vertical-align: -0.125em;
}
IEH8
IE8 poses a problem because it neither supports the checked
pseudo-class nor the box-shadow
and border-radius
that helped form our radios. The selector support can be polyfilled with a library like Selectivizr and the styles can be handled differently (perhaps a background image?) but my preferred strategy would probably be to harness graceful degradation. Sass or LESS can tersely isolate the problematic declaration blocks.
Note that enhancements to label
such as cursor: pointer
are applied to all browsers.
/* One radio button per line */
label {
display: block;
cursor: pointer;
line-height: 2.5;
font-size: 1.5em;
}
:not(.lt-ie9) {
/* HTML5 Boilerplate accessible hidden styles */
[type="radio"] {
border: 0;
clip: rect(0 0 0 0);
height: 1px; margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
[type="radio"] + span {
display: block;
}
/* the basic, unchecked style */
[type="radio"] + span::before {
content: '';
display: inline-block;
width: 1em;
height: 1em;
vertical-align: -0.25em;
border-radius: 1em;
border: 0.125em solid #fff;
box-shadow: 0 0 0 0.15em #000;
margin-right: 0.75em;
transition: 0.5s ease all;
}
/* the checked style using the :checked pseudo class */
[type="radio"]:checked + span::before {
background: green;
box-shadow: 0 0 0 0.25em #000;
}
/* never forget focus styling */
[type="radio"]:focus + span::after {
content: '\0020\2190';
font-size: 1.5em;
line-height: 1;
vertical-align: -0.125em;
}
}
Conclusion
There you have it, a solution to themeable radio controls that uses a whole lot of this…
- HTML
- CSS
… and none of this:
- JavaScript
- WAI-ARIA
- Wheel reinventing
- Voodoo
See the Pen Replacing Radio Buttons by SitePoint (@SitePoint) on CodePen.
Naturally, the basic technique could be applied to checkbox
controls as well, but you’d have to be mindful of the check (tick) design. So, what do you think? Plain unicode? Icon font? Background image? Or maybe a shape created entirely in CSS?