A Weird Imagination

Show/hide part of page with only CSS, no JavaScript

Posted in

The problem#

A common use of JavaScript is to change which content is displayed on a web page. In some simple cases this can actually be done without JavaScript. While there's some older articles on how to do this, some newer HTML/CSS features can help.

The solution#

The simplest case is the <details> tag which allows showing a foldable section without even requiring any CSS. But for more complicated cases, variations on the "Checkbox Hack" can allow CSS to express more complicated rules about what to show/hide.

The examples in that article all rely on the content being revealed being a sibling of the <input> element defining the checkbox. That's less restrictive than it sounds because the <label> element that's the actual visible part of the checkbox can be placed anywhere. But even that restriction can be loosened using :has() as shown in this example simplified from the HTML/CSS used by my previous post:

<div class="filters">
  <label>
    <input type="checkbox" id="show_completed">
    Show Completed Tasks
  </label>
</div>
<div class="todo_list">
  <ul>
    <li class="completed">some task</li>
  </ul>
</div>
.completed {
  display: none;
}
.filters:has(#show_completed:checked) ~ .todo_list .completed {
  display: list-item;
}

The details#

How it works#

The basic concept is that we the CSS display property can be changed to show/hide content. And we can give it different values based on selectors that depend on :checked or not to make a checkbox control the visibility. The complexity comes from needing to write a CSS selector relating the elements we want to control the visibility of to the checkbox element.

~ subsequent-sibling combinator#

The examples in the "Checkbox Hack" post and as I've done this before rely on the checkbox and the element to hide being siblings and using the subsequent-sibling combinator ~ to select that element:

<input id="show_completed" type="checkbox">
<li class="completed">(some content)</li>
#show_completed:checked ~ .completed {
  display: list-item;
}

Combining more selectors we can allow different structures, but the <input>'s location is still constrained relative to the element to hide:

<input id="show_completed" type="checkbox">
<ul>
  <li class="completed">(some content)</li>
</ul>
#show_completed:checked ~ ul .completed {
  display: list-item;
}

:has() pseudo-class#

Using :has()1 we can remove that restriction:

:has(#show_completed:checked) .completed {
    display: list-item;
}

That says that if our document contains a checkbox show_completed which is checked, then all elements in the document with the .completed class should be visible (as list items). There's no restrictions on where in the document the checkbox and .completed elements are. Unfortunately, Firefox gives the following warning in its developer tools:

This selector uses unconstrained :has(), which can be slow

The page I was testing on was small enough that I could not observe any such slowdown, so I'm not sure at what point that actually matters. But using a more precise selector gets rid of the warning:

.filters:has(#show_completed:checked) ~ .todo_list .completed {
  display: list-item;
}

Limitations#

While it's fancy to be able to do this with just CSS, at some point sufficiently complicated logic is probably easier to express in JavaScript (e.g., if you want the full array of filters that Sleek has or want to support custom sort orders). You could handle things like filtering on context by dynamically generating CSS for each context actually in the todo.txt file, but at that point you should probably consider whether CSS is really the right tool for the job. Or you could go all in on CSS if you really want to.

<details>/<summary>#

As mentioned above, another way to accomplish a similar task is the <details> tag:

<details>
  <summary>Click here for more info!</summary>
  More info that's hidden until clicked.
</details>

It is only for hiding a single block, and the label to control the visibility has to be immediately next to the content to hide, so it can't be used to express the logic discussed in this post. But it does cover a very common pattern on web pages, especially in contexts like lists of frequently asked questions where the summary can make it clear which section the user is interested in reading.

It is also somewhat older than :has(), with MDN saying it's been widely supported since January 2020.


  1. Note the MDN warning that :has() is only well-supported since December 2023, so may not be appropriate if you expect your users may be using older browsers. This is also why those older examples don't use :has(): they predate it. 

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.