HTML & CSS
Article

Replacing Radio Buttons Without Replacing Radio Buttons

By Heydon Pickering

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 divs and spans 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?

Heydon Pickering
Meet the author
Heydon is a UX designer who prototypes and programs accessible interfaces. He has a love/hate relationship with CSS and a lust/indifference relationship with JavaScript. He's the author of Apps For All: Coding Accessible Web Applications and you can find him as @heydonworks on Twitter.
  • http://cssmojo.com/ Thierry Koblentz

    I wrote something about this recently: http://cssmojo.com/use-radio-buttons-for-single-option/

    • heydonworks

      Good stuff!

  • LouisLazaris

    Nice!

    • http://css-101.org/ Thierry Koblentz

      Thanks Louis

  • http://bittersmann.de/ Gunnar Bittersmann

    “I love the adjacent sibling combinator…”
    So do I. Some browsers (namely Android) do not, cf. http://css-tricks.com/webkit-sibling-bug/ Android 4 is still affected. And the proposed hack has severe performance issues on some devices. I don’t have a solution, unfortunately.

    “We will, of course, have to add span elements to the markup…”
    We actually don’t have to.
    Look ma, no span!
    (Input elements should have an ID anyway, QA would love them for automated tests.)
    With regards to semantics, this markup makes more sense to me than input wrapped in label since an input control is not part of its label.
    When you want a container around input and label you’d end up with the same amount of elements, though.

    “border-radius: 1em;”
    Make this inpedendent from width/height, use 50% (or higher percentage which would be rendered the same). When later changing the size, you will not want to have to touch the border radius.

    • heydonworks

      I’ve never had adjacent sibling issues with Android. Thanks for the tip; will do some testing.

      I should have written “we will of course have to add s to _this_ markup” :-P Your version is terser +1

      I don’t follow you about the border-radius value. Ems scale too, so if you change the parent font size, the radio buttons remain intact / round without editing them directly. It acts just the same as 50%, but I keep my units consistent.

    • http://www.joezimjs.com Joe Zimmerman

      Yea, I forked his Code Pen to do this same thing: removed the need for the span and fixed the border-radius as well. I also decided to change the visual styles because I think it’s ugly.

      http://codepen.io/joezimjs/pen/yBgCD

    • Erika

      Looks like the adjacent sibling bug is fixed.

  • http://www.fastconversion.com/ Chandan kumar

    I know with html5 and css3 many more things can be done. This article is really helpful for newbie Great Post! If anybody is looking for assistance on html and css they can get in touch with me. I will be more than glad in having such discussions with you.

  • Koter

    Thank you for this beautiful solution!

  • Craig Buckler

    Very nice. I did a similar thing a while ago:
    http://www.sitepoint.com/better-css3-toggle-switches/

    Rather than resort to JavaScript solutions for IE8 and below, I used the :empty selector to disable the effect in those browsers.

    Issues remain with older Webkit browsers which don’t fire state changes, but there is a JavaScript workaround. Think I’ll need to write about that one…

  • LouisLazaris

    Well, what you consider not ugly is what many would consider poor contrast, so it’s subjective really. I think both look fine, and I actually like Heydon’s with the familiar dot-within-circle look of a standard radio button.

    • http://www.joezimjs.com Joe Zimmerman

      I totally understand. I mostly wasn’t impressed with the colors and the super big/fat black lines. The only reason I didn’t do the dot inside the circle is because I didn’t want it to depend on box-shadow. Though, I guess, :checked has pretty much the same support as box-shadow with the exception of some mobile browsers, so I guess I could depend on it more.

    • http://www.joezimjs.com Joe Zimmerman

      Also, I just realized, the styles look a lot better in Firefox (that’s what I was developing with). The issue is the difference between FF and Chrome’s sizes with sub-pixel sizes. So it might have looked really lame in Chrome if that’s what you used.

  • LouisLazaris

    Good points, Gunnar.

    For the extra element, you really can’t avoid that in most cases. You usually want something constraining the different input/label pairs which is why I gather Heydon says ‘we have to add span elements’. As you pointed out, there’s no difference in the amount of elements.

  • Johan

    That is exactly how I would do it. Good stuff ++

  • Gerard van Beek

    Very impressive. Although you claim not to use Voodoo, it looked like it to me at first.

    • biteljuz

      lol! Nice comment :)

  • Bob Dickow

    I’m not quite that impressed. Why replace the easily understood standard radio buttons, which look perfectly ok, with large, slow, gimmicky, overdone, naïve-looking radio buttons. Yeah, I know. Style ’em the way I want. …which is standard good ol’ radio buttons.

  • http://bittersmann.de/ Gunnar Bittersmann

    With “changing the size” I didn’t have changing the font size in mind, but the size of the element. With { width: 3em; height: 3em; border-radius: 1em } it wouldn’t be a circle anymore (without changing border-radius as well), with { width: 3em; height: 3em; border-radius: 50% } it would.

    • heydonworks

      Oh I see. Sure. I wasn’t anticipating that. You’re right, of course. I wonder if I would have to adjust the vertical-align value too, in that case.

  • Adam

    If you’re concerned about accessibility or usability then don’t forget disabled states!

    [type=”radio”][disabled] + span {
    color: #999;
    }
    [type=”radio”][disabled] + span:before {
    border: 0.125em solid #fff;
    box-shadow: 0 0 0 0.15em #999;
    }

  • Joy

    Is there a way to use this technique and have an image instead of a radio button? I’m trying to make radio buttons that are a clickable image but the technique I’m using now uses the label to display the image and hides the radio button so the keyboard controls do not work. I was hoping I could modify this to use an image and still be able to use the tab and arrow keys to move from one radio button to the next. So far I haven’t figure it out.

  • Keith Patton

    Anyone know why they are called “radio buttons”? If you had a car old enough to have a radio with manually set “buttons” the kind where the station changer was powered by nylon string. that ran around a pulley on the knob, then you know. You pulled the button out found the station then pushed it in, clamping the button to a place on the nylon string. When you wanted to reset to that station you pushed the button “in” and it pulled the station changer to that preselected location. You had a couple buttons labeled AM, and two for FM, it you had a dual band radio. You could set them all one band or the other, but it might confuse those not familiar with your listening habits. Analog is what you call it. In use up till the 1980’s. I still have it on my 79 MGB radio. Now you know the rest of the story. Old guy signing off.

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

    • http://cssmojo.com/ Thierry Koblentz

      Back in the day, we avoided implicit labelling (controls inside labels) because Internet Explorer failed to focus on the control when user clicked on its label. Not sure when MS fixed that (in which version of IE things start working)…

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Tobias Baunbæk Christensen

    Awesome! I created something similar, but couldn’t get accessibility to work. You clearly fixed that.

    One change though. You can avoid the for/id attributes. Which I think is one of the great advantages of doing input’s inside label’s and something I really need. Unless I’m missing a point with them?

  • Martin Ockovsky

    thank you very much for this great article

  • http://balramsingh.in Balram Singh

    very well done, thank you :)

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Front-end, once a week, for free.