Toggling browser elements without JavaScript

Monday 21 March 2016 at 08:00 GMT

I'm having a sleepy weekend, and I feel like the rhythm of one week on Docker, one off is about as much as I can handle. (Those posts take work!) So we're going to have a week of random stuff before I get back to it. Hope you don't mind the wait. :-)


Last week, I was working on a website at work, and I used a useful trick that I came up with a few years ago. I'm sure I'm not the first person to figure this one out, but I think it's cool and not everyone had heard of it before, so I thought I'd write about it.

It's a fairly common thing in UIs for something to be toggled on or off. For example, we might be showing and hiding a navigation menu, or booking a table at a restaurant online.

On the web, it's pretty typical to use JavaScript for this. Here's a restaurant booking example that uses JavaScript to change CSS classes as we change our minds about the time. In practice, we'd also store the selected time in a variable or hidden form element so that we can submit the selected option to the server later.

You can read the code, or enable the Codepen in order to try it out and watch the .selected style apply to the selected item.

The following code can be viewed and run interactively using Codepen by clicking the button below. Note that enabling this will load third-party code into this website, which may involve setting cookies or other such nonsense.

<ul class="time-picker">
  <li>19:00</li>
  <li>19:30</li>
  <li>20:00</li>
</ul>
body {
  font-family: sans-serif;
}

.time-picker {
  display: grid;
  justify-items: center;
  margin: 0;
  padding: 0;

  li {
    margin: 1rem;
    padding: 1rem 2rem;
    border: 1px solid black;
    border-radius: 1rem;
    list-style: none;
    cursor: pointer;

    &.selected {
      background-color: #4caf50;
      color: white;
    }
  }
}
const picker = document.querySelector(".time-picker");
const items = picker.querySelectorAll("& > li");
for (const item of items) {
  item.addEventListener("click", (event) => {
    for (const otherItem of items) {
      otherItem.classList.remove("selected");
    }
    item.classList.add("selected");
  });
}

Now, there's two problems here. One is that we're not using semantic HTML. The second is that we're using JavaScript, and I'd rather avoid it.

JavaScript does one thing here that CSS can't: it stores the state of the world so that it can look it up next time. Because we can use CSS classes in the DOM, as well as variables in the JavaScript execution context, to store state, we can make decisions based on past events. You can't do that with pure styling.

However, there's other ways to store things in the DOM.

This is really a form, and as such, should have <input> elements. As they're options, we probably want radio buttons. Something like this:

<input id="time-1900" type="radio" name="time" value="19:00"/>

We can set those up… but they look pretty boring.

The following code can be viewed and run interactively using Codepen by clicking the button below. Note that enabling this will load third-party code into this website, which may involve setting cookies or other such nonsense.

<form class="time-picker">
  <label for="time-1900">
    <input id="time-1900" type="radio" name="time" value="19:00" />
    19:00
  </label>
  <label for="time-1930">
    <input id="time-1930" type="radio" name="time" value="19:30" />
    19:30
  </label>
  <label for="time-2000">
    <input id="time-2000" type="radio" name="time" value="20:00" />
    20:00
  </label>
</form>
body {
  font-family: sans-serif;
}

.time-picker {
  margin: 1rem;

  label {
    display: block;
  }
}

However, now we have state. Those radio buttons are checked and unchecked, and we can detect this in CSS with the :checked pseudo-class selector. We can use that to trigger a style change to a child node with a chained selector, to a sibling with the + or ~ sibling selectors, or even to a parent using the :has selector.

label:has(input:checked) {
  background-color: #4caf50;
  color: white;
}

As the <label> triggers the radio button, we can hide the button and just keep the label. A bit more styling and voila: we have an example that looks exactly like the first one, but uses semantic HTML and CSS instead of JavaScript.

The following code can be viewed and run interactively using Codepen by clicking the button below. Note that enabling this will load third-party code into this website, which may involve setting cookies or other such nonsense.

<form class="time-picker">
  <label for="time-1900">
    <input id="time-1900" type="radio" name="time" value="19:00" />
    19:00
  </label>
  <label for="time-1930">
    <input id="time-1930" type="radio" name="time" value="19:30" />
    19:30
  </label>
  <label for="time-2000">
    <input id="time-2000" type="radio" name="time" value="20:00" />
    20:00
  </label>
</form>
body {
  font-family: sans-serif;
}

.time-picker {
  display: grid;
  justify-items: center;
  margin: 0;
  padding: 0;

  label {
    display: block;
    margin: 1rem;
    padding: 1rem 2rem;
    border: 1px solid black;
    border-radius: 1rem;
    list-style: none;
    cursor: pointer;

    &:has(input:checked) {
      background-color: #4caf50;
      color: white;
    }
  }

  input {
    display: none;
  }
}

As much as I like instructing the computer what to do, I prefer to declare the state of things and have my environment—in this case, my browser—figure it out. My procedural code needs testing, maintaining and debugging. HTML and CSS need none of that.


If you enjoyed this post, you can subscribe to this blog using Atom.

Maybe you have something to say. You can email me or toot at me. I love feedback. I also love gigantic compliments, so please send those too.

Please feel free to share this on any and all good social networks.

This article is licensed under the Creative Commons Attribution 4.0 International Public License (CC-BY-4.0).