Save Expand icon

Ron Valstar
front-end developer

Front-end logic without Javascript

It is surprising how few front-end developers are aware of this little trick (but maybe I meet the wrong ones).
There is an easy way to manage the state of a user interface using only HTML and CSS.
You can easily make expanders, tabs, hamburger menus or even apply multiple complex states onto an HTML element, and everything without a single line of JavaScript.

The basics

To get started you use the CSS pseudo selector input:checked in combination with a sibling selector + or ~. This can be used to have a different element reflect the state of the input.

For instance if you check the checkbox the text in the div will turn red:

<input type="checkbox">
<div>red</div>
input:checked + div { 
  color: red;
}

The input:checked state applies only to checkbox and radio types.
You can do this with other siblings as wel or their children.

Add a label

The input must always precede the targeted element. But we can get around that by hiding the input and triggering it with a label. But we cannot simply display:none the input because that would make it stop working. Instead we add a class called visuallyhidden (by convention).

<input type="checkbox" id="you" class="visuallyhidden">
<div>red</div>
<label for="you">green</label>
input:checked + div { 
  color: red;
}
.visuallyhidden {
    border: 0;
    clip: rect(0 0 0 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
}

Simple examples

At this point you know enough to apply this to numerous UX patterns. Here are some examples.

An expander

An element that reveals more content when clicked.

Tabbed panels

To created a tabbed interface we use a radio instead of a checkbox. If you check the source you might notice that the sections all have the className visuallyhidden. The reason is that, contrary to display:none, hiding it this way still allows for search engines to read it's content.

A hamburger menu

This is really just an expander. But just for examples sake: here is a nested one. And a bit of UX advice on hamburgers: try not to use them especially if you only have three menu items.

Here we have is also a small issue that is hard to resolve without resorting to Javascript. The submenus in this example use input[type=radio] and unlike type=checkbox radios cannot be disabled when clicking the selected one. Resolving this issue requires adding an extra unrelated input which will be checked by Javascript when a checked radio is clicked.

Note that this example is merely to illustrate the technique. There are better ways of showing content than using a carousel.

It's easier with variables

These sibling>child selectors can get a bit tedious or complex. And what if you start moving stuff around, or try to generalise it into a component.

Luckily CSS also has variables. We can also use those and make things even more interesting.

:root {
  --color: green;
}
input:checked ~ main { 
  --color: red;
}
.text {
  color: var(--color);
}

This might not look much different from what we started with but variables allow you to have only one :checked declaration that applies to multiple var(...) implementations and (even better) multiple different :checked declarations can be used in a single rule: transform: translate(var(--x), var(--y));.

If you use variables you can declare them as high as possible and never worry about moving your variable affected components around. So put your hidden inputs at body>input and overwrite them at body>input~main for instance.

Here's a little box. It is a relatively simple example how three individual states can amount to sixteen possibilities. Click on the side to rotate it. Click on the top to open it. And click the inside to look inside.

The topleft checkbox shows all the labels and checkboxes.