Skip to navigation Skip to content

HTML native interactive controls using CSS and flexbox

Five-minute read

I find myself drawn to clever solutions for common problems.

One recent design I built called for side-by-side buttons. The user could click a button, which would maximize while other the choices would minimize. One solution would be declare CSS styles – btn -- default and btn -- active – and then toggle states on click using JavaScript or jQuery.

Even though the controls were buttons visually, functionally they behaved like radio buttons, something devs already have in their HTML toolbox.

Styling forms can be tricky, but it is possible to apply a little CSS sorcery to style radio buttons like any other element. The buttons themselves are hidden and the styles are applied to clickable labels.

Not only does this allow design freedom, presentation stays in the CSS and more semantic markup is possible – a big deal for those who depend on screen readers. Strip away the CSS and any JS, and the functions of the buttons would be entirely unchanged.

Here’s the basic markup for two side-by-side buttons. The <span> allows button text to be targeted. But any markup would work.

    <input id="btn-one" type="radio" name="button" value="one" />
    <label for="btn-one" class="btn">
        <span>Label text</span>

    <input id="btn-two" type="radio" name="button" value="two" />
    <label for="btn-two" class="btn">
        <span>label text</span>

The first step is to hide the radio button itself. Don’t use display:none or aria-hidden because the buttons need to remain accessible:

    [type="radio"] {
         border: 0;
         clip: rect(0 0 0 0);
         height: 1px; margin: -1px;
         overflow: hidden;
         padding: 0;
         position: absolute;
         width: 1px;

Next, style the label to look like a button. Use + to target the first <label> after the radio.

    [type="radio"] + label {
         margin:0 .125rem;
         padding:2rem 0 0;
         text-align: center;
         transform: scale(.8);
         transition:transform .3s cubic-bezier(.94,-0.66,.33,2.56) .1s;

This creates a default button. The next step is the active state using :checked.

    [type="radio"]:checked + label {
         transform: scale(1.1,1.1);
         transition:transform .3s cubic-bezier(.94,-0.66,.33,2.56);

These “buttons” can be styled any way they need to be just like any other element. Transitions add some bounce between states.

Here’s the result:

See the Pen pJXmLx by David Putney (@putneydm) on CodePen.

###Wait, there’s more …

Combining styled radio buttons with flexbox source order controls adds even more options.

Another design called for a three-option selector. Click on the button and the selector would accordion open to show additional options. Click an option and the selector would accordion closed and only the active option would remain visible.

The markup for the buttons is the same as previously shown, with one addition.

    <div class="btn-wrapper">

         <input id="btn-one" type="radio" name="button" value="one" />
         <label for="btn-one" class="btn">
             <span>Label text</span>

         <input id="btn-two" type="radio" name="button" value="two" />
         <label for="btn-two" class="btn">
             <span>label text</span>

         <input id="btn-three" type="radio" name="button" value="two" />
         <label for="btn-three" class="btn">
             <span>label text</span>

Because flexbox is needed for the buttons to work properly, they are inside a flexbox wrapper and arranged in a column that is justified at flex start.

    .btn-wrapper {
         justify-content: flex-start;
         margin:2.5rem auto;

Flexbox source order is how the magic happens. The radio buttons will default to a source order of 1, 2, 3, … . If the checked radio button is a assigned a source order of -1 it will always be ahead of these other elements.

    [type="radio"]:checked + label {

A few lines of jQuery/JS adds the accordion action. Here’s the result:

See the Pen yNdmdp by David Putney (@putneydm) on CodePen.

After shifting so much functionality from JS to native CSS/HTML, it may seem like a bit of a letdown to throw in JS at this point. But, the goal here isn’t to cut out JS entirely but to use the best tool for the job. Anything that can be kept native probably should be.

And, with so much browser support for CSS/HTML native these days, exploring options can yield some clever solutions.