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:
display: none
— completely removes an element from the layout, removing it from tab order, accessibility tree, and so on.visibility: hidden
— an element is still present in the layout, but is not visible, does not receive focus, and is absent in the accessibility .opacity: 0
— similarly tovisibility
an element keeps its place in the layout, but continues to receive focus & events, and will be present in the accessibility tree.
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
<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:
- Initially, the inner
<ul>
is hidden viavisibility: hidden
. - Then, it is shown by
visibility: visible
when we hover over a menu item. - Then, when we click on any of the items, the whole example wrapper gets a
visibility: hidden
applied to it. - 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 */
}
- We can’t use
inherit
here, as we need to break from the parent<ul>
’s visibility in this case. - We can’t really use the “inverted” method — we could use it for some cases where we know the exact structure of our HTML, where we could target the specific conditions at which we need to hide everything, but this would be far from being practical.
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:
- If the goal is to restore visibility, invert the logic, only using one rule with the
visibility: hidden
. - Alternatively, replace the
visible
withinherit
if the selectors are too complex. - If the element must be
visible
, instead of setting the value right away, use it as the value to a CSS variable, providing an API for your component that other components could use to hide your element reliably.
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.
Published on with tags: #Practical #CSS Variables #CSS