Skip to content

Linked radio buttons with CSS

Screenshot of styled radio buttons
Regular form elements, heavily styled.

Demo: codepen.io/shellbryson/pen/KWKyXN

It’s a pattern we see fairly often across the web: a series of ‘buttons’ connected by lines to indicate procession, ratings or steps. The trick is to make it as flexible and accessible as possible, and this is perfectly achievable without JavaScript.

Using regular form elements as our base, we can use a little extra markup and some trickery with CSS ::before and ::after to create attractive (or… excessively pink) results without sacrificing the underlying accessibility.

The HTML

So we start with a regular ordered list:

<ol class="nodes">
 <li class="node">
   <input class="radio" type="radio" id="r1" name="radio-set">
   <label class="label" for="r1">1</label>
 </li>
 <li> ... </li>
</ol>

We’re using accessible markup, making sure we have a label and that the label is referencing the field (via for=). Any interaction with the label will be passed onto the field itself, which is key as we’re going to play with the label to give us our fancy inputs.

The CSS

We take the label, and use ::before and ::after to position circles on top of the real form input to mask it, and getting our desired look. As these pseudo-elements are children of the <label>, any interaction with this label is passed through to the form field below. We simply layer things up:

 

 

 

.label {
 ...

 &::before {
  display: block;
  position: absolute;
  left: 0; right: 0; bottom: 0; top: 0; // trick to fill element
  margin: auto;
  z-index: 1; // ensures this is displayed above form field
  width: 60px;
  height: 60px;
  content: " ";
  border-radius: 50%;
  background-color: pink;
 }
 
 &::after {
  ...
 }
}

The final step is ensuring that the :checked state of the radio button is reflected by our fancy styling:

 .radio:checked + .label::after {
   background-color: white;
 }

As we want our list to adapt to the space available in the browser, we can use css flex with justification property to ensure each li is evenly spaced:

.node {
 display: flex;
 justify-content: space-between;
}

By adding an additional  ::after on the .node itself, we can join the nodes together. Note, we’re deliberately offsetting the start of the pseudo-element so that the line stretches from the centre of the element to the right edge of the <li>.

.node::after {
  display: block;
  position: absolute;
  left: 60%; right: 0; bottom: 0; top: 50%;
  content: " ";
  height: 6px; width: 100%;
  background-color: lighten($color-base, 20%);
 }

Finally, you’ll need to remove the line from the last child as we do not want a line going off into nowhere:

.node:last-child {
  &::after {
   display: none;
  }
}

Interaction and animation

By adding transforms and transition delays we can achieve all kinds of over-the-top effects. Advice: don’t over use this, remember the UX.

Go have a play (CodePen)

Dev notes

We’re making heavy use of display:flex and positioned elements here, and while it all remains flexible across most modern browsers, flex can be rather buggy. You could probably achieve much of this using CSS display:table, or if you were daring, regular float.

Leave a Reply

Your email address will not be published. Required fields are marked *