bscure CSS: Restoring Visibility

One of the things I love about CSS is how some of the properties hide a lot of depth inside. For that reason, visibility was always one of my favorites. In this article, I will talk about one of its interesting aspects and will propose an idea: what if we would never use visibility: visible?

imilarities and Differences

CSS is known for having multiple ways to achieve any given goal. It can lead to different properties being hard to distinguish from each other, leading to misconceptions or misunderstandings, resulting in using an incorrect tool for its job, which, in place, could lead to bugs.

Visibility property can often be comparedGo to a sidenote to two other properties: display, and opacity. All three properties can be used to “hide” an element:

When we’re hiding an element with display: none and opacity: 0, all of their children also become hidden, and there is no way to restore them.

Visibility is different. If a parent element is hidden with it, but then we will set visibility: visible on any of the descendant elements, this descendant’s visibility will be restored, and it will appear where it should be, regardless of its parent visibility value.

Here is how it is defined in the specs (emphasis is mine):

visible
The generated box is visible.
hidden
The generated box is invisible (fully transparent, nothing is drawn), but still affects layout. Furthermore, descendants of the element will be visible if they have visibility: visible.

Now, let’s look at an example:

xample

Hide using:
    <div class="is-hidden">
      <p>I should be always hidden</p>
      <p class="is-visible">I could be visible!</p>
      <p>
        Only
        <span class="is-visible">this span</span>
        might be visible.
      </p>
    </div>

We can play with the above example, changing which property is used for hiding or showing the elements — visibility is a noticeable outlier!

While we could use this aspect of the property to achieve something goodGo to a sidenote, quite often, this could lead to unexpected results.

he Problem

If the visibility: visible is only used to restore the visibility, this can lead to an issue with two independent components, where we could need to hide the wrapping one, but the one inside will have some descendant’s visibility restored. With visible value, this inner element will become visible — which could not be what we would expect!

To reproduce the problem in the following example, you’d need to hover over any menu item and then clickGo to a sidenote on it.

After clicking on any item, if you won’t move your cursor, you should see the example wrapper disappear while the menu items would stay visible.

Why this happens:

  1. Initially, the inner <ul> is hidden via visibility: hidden.
  2. Then, it is shown by visibility: visible when we hover over a menu item.
  3. Then, when we click on any of the items, the whole example wrapper gets a visibility: hidden applied to it.
  4. But because we’re still hovering over the list when this happens, we’d see the menu items, as, regardless of the list’s ancestor being hidden, it is kept visible due to the visibility: visible.

Moving the cursor out of the list hides it, so it won’t reappear, as there’d be nothing left to hover.

Here is what the CSS responsible for hiding/showing items looks like for this example:

.example--broken a + ul {
  visibility: hidden;
}
.example--broken a:is(:hover, :focus) + ul,
.example--broken ul:is(:hover, :focus-within) {
  visibility: visible; /* This can lead to a bug */
}

he Solutions

We can see how this behavior could be problematic on dynamic sites — and cases like this are something I encountered in my practice multiple times. The fixes for this — as soon as we’d identify them — are usually pretty simple, though.

ide-only Selectors

The easiest way to make “restoring” visibility not cause bugs is to never have to restore it! Here is how we can change the above CSS:

.example--hide-only a:not(:hover, :focus) + ul:not(:hover, :focus-within) {
  visibility: hidden;
}

What we’re doing here is “inverting” the logic for the rule that previously did revert the visibility, and only use one CSS rule to hide the content, listing the exact conditions for this. Here is the example fixed by this method:

estoring via Inheritance

While I’d argue that if we could write just one rule that does the job properly, we should do it, sometimes we cannot modify a selector, or doing so could be inconvenient for some reason. Can we keep the “hiding” and “showing” rules separate, restoring the visibility via an override, but without it causing problems?

Sure, we can! All we need to do is to use the inherit value instead of the visible!

.example--inheritance a + ul {
  visibility: hidden;
}
.example--inheritance a:is(:hover, :focus) + ul,
.example--inheritance ul:is(:hover, :focus-within) {
  visibility: inherit;
}

By using inherit hereGo to a sidenote, we are making sure that the visibility of our item would match the visibility of the parent — so if the parent is hidden, our element would be also hidden, and if the parent is visible — our element would be visible as well.

Here is our example, now fixed by using inheritance.

un Fact from the Past

There is a low chance this would be useful to anyone today (unless we’d travel to the past somehow and then would need to write CSS for IE6…), but anyway!

If we’d look at “Can I Use…” for the global inherit keyword, we will see that IE6-7 did not support it… But you know what? The visibility: inherit method did actually work in IE6! All because it did apply an unknown value instead of falling back to the previously valid one and treated the unknown inherit as the implicit initial one, conveniently working as intended!

sing CSS Variables

But what if we’d want to use visibility: visible to enforce the visibility inside some hidden context? And then we’ll still want to hide everything inside some other context with visibility: hidden on it?

Let’s say we’d modify our last fixed example, but this time we will add an element to the start of each list item that mustGo to a sidenote be visible even when the items are not hovered:

In this case, we get a broken result when we clickGo to a sidenote on any of the items, where the added ellipsis pseudo-elements are visible even when we hide the parent. Here is how we show them in the first place:

.example--with-visible-items ul a::before {
  content: "…";
  visibility: visible; /* This can lead to a bug */
}

However, if we’re responsible for setting visibility everywhere, we can use CSS variables to achieve this! Look at this:

.example--with-visible-items ul a::before {
  content: "…";
  visibility: var(--visibility-override, visible);
}

Instead of setting the visible value right away, we can use an intermediate CSS variable as an API that other components could use to hide this element on demand. This way, we’d only need to modify the place that applies the visibility: hidden to hide everything, adding a definition of our CSS variable alongside a regular visibility.

Here is the above example fixed by using CSS variables:

CSS variables can be very useful, allowing us to fix problems we could not resolve in other ways (or which could require much more complex and less reliable and maintainable code) — this is just another proof of that.

onclusion

Every time you will see a visibility: visible in your or your colleagues' code, try to understand what it is trying to do. Then, either:

Maybe it could also be helpful to introduce a stylelint rule prohibiting the visibility: visible declaration while allowing inherit or usage as a part of CSS variables fallback!

And, as the last note — we can apply these methods to other similar cases: to most inherited properties like color, font-size (think of font-size: 0; ), pointer-events, and others.

Even more — many things in this article are universal, like the “inverted selectors” method or using CSS variables as an API for specific properties. We can put them into our toolboxes, not just as a solution to a specific visibility problem but as something that could help in many other cases.


Let me know what you think about this article on Mastodon!