There Are a Lot of Ways to Hide Stuff in the Browser
Tags: css, a11y, html
Published: 2023-12-06 02:43:11

---
title: There Are a Lot of Ways to Hide Stuff in the Browser
published: true
date: 2023-12-04 13:32:24 UTC
tags: css, accessibility, html
canonical_url: https://macarthur.me/posts/hide-stuff-in-the-browser
---

I stumbled across a pull request in Marc Grabanski's [modern-todomvc-vanillajs](https://github.com/1Marc/modern-todomvc-vanillajs/pull/26?ref=alex-macarthur) from some time ago, where I was first introduced to the `hidden` DOM attribute. It's a simple way to hide something on a page that [shouldn't be seen](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden?ref=alex-macarthur) in any presentation, It's been around forever, and I had no idea it existed.

I took a quick mental inventory before thinking: "wow, there are a lot of ways to hide stuff in the browser." And after a little investigation, they all seem have to have some really important trade-offs, strengths, and purposes.

I knew I'd benefit from doing an overview of the big ones, so that's what's happening here. We're reviewing some of the most common tools designed to hide things in the browser.

## #1: display: none;

You've seen this one a lot – it's a classic. When applied to an element, it'll completely remove it from the flow of the document. You won't be able to see it, and it'll cease to take up any space on the page.

Here's a simple grid with three 1:1 blocks:

![three boxes next to each other](https://cms.macarthur.me/content/images/2023/12/image-9.png)

When we throw `display: none` on the second list item, the third box folds in as if it were never there:

![second item is removed from layout](https://cms.macarthur.me/content/images/2023/12/image-11.png)

From an accessibility standpoint, hiding an element like this entirely [removes it from the accessibility tree](https://developer.mozilla.org/en-US/docs/Web/CSS/display?ref=alex-macarthur#accessibility_concerns), meaning tools like screen readers won't be able to access it anymore either, and it won't be focusable or findable.

Worth noting, the rendering state of that element **is not cached,** which can have performance implications. More on that in a bit.

## #2: opacity: 0;

Here's another common one. When you reduce the opacity of an element to nothing, it's visually hidden, but it still exists in the document flow, and it's still focusable & accessible to screen readers.

You can see this when the second box has its opacity removed. It's not visible, but still gets focus when tabbing through (see the bold text below the boxes):

![second item is still focusable even when there's no opacity](https://cms.macarthur.me/content/images/2023/12/opacity-focus.gif)

Using `opacity` also has the added benefit of being easy to directly animate – something the `display` property lacks, making it common to use in hover effects.

## #3: visibility: hidden;

Using `visibility: hidden` is very similar to turning off an element's opacity, except _the contents are no longer focusable or otherwise accessible_. Here's the same set of boxes, but this time, the second item is hidden with `visibility: hidden`.

![the second item is no longer focusable with 'visibility: hidden'](https://cms.macarthur.me/content/images/2023/12/visibility-focus.gif)

As you can see, the hidden box is no longer focusable. It's skipped over completely. You won't be able to use the browser's search functionality to find it either.

## #4: content-visibility: hidden;

Here's a newer one that's a big more interesting. It [still lacks full browser support](https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility?ref=alex-macarthur#browser_compatibility), but is largely intended as a tool for improving rendering performance. There was a lot of buzz around using `content-visibility: auto` for [improving the performance](https://web.dev/articles/content-visibility?ref=alex-macarthur) of long, content-heavy pages a couple years ago, but it can also be used to completely hide things declaratively.

Using `content-visiblity: hidden` on an element won't hide it entirely, but only its _contents_. In many cases, the result seems largely identical to using `display: none`, but there's a key advantage: _the element's rendering state is cached_ even when it's not visible. This makes it a great choice for optimizing the rendering performance of toggled elements (settings menus, modals, etc.).

### The Benefit of Cached Rendering

I confirmed all this with my own little setup. All it consisted of was a button and a box. I also used a some JavaScript to generate a ton of text within the box before I did any toggling, just to give the browser a little more to actually render.

```
<style>
.is-hidden {
	/* display: none; /*

	/* vs. */

	/* content-visibility: hidden; */
}    
</style>

<button id="button">Click me</button>

<div id="boxWrapper">
	<div id="box"></div>
</div>

<script>
	const box = document.getElementById('box');
    
    // Stuff it with 10k <div>s.
	for (let i = 0; i < 10_000; i++) {
		const div = document.createElement('div');
		div.innerText = 'Hi!';
		box.appendChild(div);
	}
</script>
```

Then, I wired up that button. On click, I measured how long it took to toggle the box 100 times. If I did all of this synchronously, the browser would batch the DOM changes between repaints, bypassing the point of our experiment. To get around that, the toggling is spaced around the repaint cycle using a couple of `requestAnimationFrame()` calls. For some more information on why this works, I wrote about it [in more depth here](https://macarthur.me/posts/when-dom-updates-appear-to-be-asynchronous?ref=alex-macarthur).

```
const button = document.getElementById("button");

button.addEventListener("click", async () => {
	const start = performance.now();

	for (let i = 0; i < 100; i++) {
		await new Promise((resolve) => {
			requestAnimationFrame(() => {
				requestAnimationFrame(() => {
                    
					// toggle after every repaint
					boxWrapper.classList.toggle("is-hidden");
					resolve();
				});
			});
		});
 	}
    
	console.log("ms:", performance.now() - start);
});
```

The difference was very clear, and only increased as the number of elements to render grew. First, I tested with a the 10,000 `<div />` mentioned above. Using `content-visibility` reduced the total amount of time by over 30%:

| Approach | Milliseconds |
| --- | --- |
| display: hidden; | 4,948 |
| content-visibility: hidden; | 3,332 |
| | **_~33% faster_** |

Out of curiosity, I then bumped up number of `<div>`s to 100,000. This time around, the performance impact was even more bonkers:

| Approach | Milliseconds |
| --- | --- |
| display: hidden; | 47,216 ms |
| content-visibility: hidden; | 7,495 ms |
| | **_~84% faster_** |

Chrome's performance tooling also shows a big difference between the two. I let both approaches run for 10 seconds, getting the following two summaries from the performance audit. I knew it'd be an imperfect scientific test, but I was hoping it'd still reveal a clear winner.

First, using `display: none`:

![using 'display: none' causes lots of rendering](https://cms.macarthur.me/content/images/2023/12/image-7.png)

And second, using `content-visibility:`

![](https://cms.macarthur.me/content/images/2023/12/image-8.png)

As you can see, there's a stark difference in the amount of rendering work being done – 83% less – confirming all those `<div>`s are being pulled from the rendering cache each time they need to be shown again.

### What about browser support? 

At the time of writing this, `content-visibility` [doesn't have incredible browser support](https://caniuse.com/?search=content-visibility&ref=alex-macarthur). It sits at about 74% global support, with Firefox and Safari being the stragglers. Still, CSS's `@supports` at-rule makes it simple to set up a `display: none` fallback:

```
.is-hidden {
	display: none;
}

@supports (content-visibility: hidden) {
	.is-hidden {
		display: initial;
		content-visibility: hidden;
	}
}
```

With the numbers I saw and the simple `display` fallback, I know I'll be reaching for this more often when building out components that'll show and hide for various purposes.

## #5. the 'hidden' attribute

Here's the one that started this curiosity train. The `hidden` attribute is intended to be simple way to tell browser to hide an element _without CSS_. More than anything, it's about semantics.

Philosophically, CSS is primarily concerned with the _presentation_ of elements on a page, and not so much with their semantics. In fact, the original documentation for the language are [riddled with this sort of language](https://www.w3.org/TR/REC-CSS1/?ref=alex-macarthur#basic-concepts), and the more modern "snapshot" documents [maintain the same sentiment](https://www.w3.org/TR/CSS/?ref=alex-macarthur). Read through it, and you'll see a lot of verbiage like this (I've added emphasis):

> CSS is used to describe the **presentation** of a source document, and usually **does not change the underlying semantics** expressed by its document language.

So, consider something that might need to be rendered on a page eventually, but doesn't always belong there semantically – like an error message. Using `hidden` in cases like this offers some benefits.

Most obviously, it's simpler to implement. No inline styles or one-off "is-hidden" CSS classes you need to include in your stylesheets. The attribute doesn't even need to have a value. It just needs to exist:

```
<!-- Won't be rendered! -->
<div id="error" hidden>This is an error message!</div>
```

And when it does need to be shown, the amount of JavaScript you need to write is more elegant too (at least to me):

```
const errorEl = document.getElementById('error');

errorEl.removeAttribute('hidden');

// This works too!
// errorEl.hidden = false;
```

In all, it's a nice separation of concerns. Leave your CSS to the stylistic, presentational stuff, and the `hidden` attribute to whether it's appropriate to show something on the page (yet).

### A "hidden" Superpower

Under the hood, the `hidden` attribute is very similar to using `display: none`. It removes the element from the accessibility tree, preventing screen readers from accessing it, as well as the browser's "find" functionality.

But by using a special attribute value – "until-found" – you can "break out" of that hidden state as soon as the item's been found using the browser's search functionality (or "command + F"). Here's a really simple demonstration of the feature with the following markup:

```
<h2>secret content below:</h2>

<div hidden="until-found">
	<div>You found it!</div>
</div>
```

It's hidden at first, but as soon as I search for "found," it shows up:

![text is hidden until it's found](https://cms.macarthur.me/content/images/2023/12/hidden-until.gif)

This makes is a great tool for components like an FAQ accordion. On page load, the content can be visually hidden. But if a user's searching for a specific string, it can be made immediately visible.

The only downside to this mechanism is its browser support. Currently, only [Chromium-based browsers](https://caniuse.com/?search=until-found&ref=alex-macarthur) appear to respect it.

## Some Honorable Hacks

There's a handful of other tactics out there used to hide items, but in my opinion, they're better relegated to "hacks" in most cases.

For example, you can hide something by zeroing out its height & width, sending it far off screen with creative `absolute` positioning, or setting a `z-index` value to negative eternity.

I'm sure I've missed a few others, but regardless, despite their value in certain scenarios, they're typically not designed _for the explicit purpose_ of hiding things. And for that reason, I chose to bypass them here.

## How should you hide your elements?

There are a million different circumstances out there, each with their own quirks and exceptions, but here's a quick run-down of how I'd reach for the different approaches explored here:

- Use `display: none` when you want to completely remove an element from the page layout and accessibility tree. Especially when it's intended to be a permanent removal, since its rendering isn't cached.
- Use `opacity: 0` when you want to preserve page layout and accessibility, and just want to make the element invisible. 
- Use `visibility: hidden` when you'd like to preserve page layout, but you _don't_ want it to be accessible (and therefore focusable + findable). 
- Use `content-visibility: hidden` when you'd like to completely remove something from the page and accessibility tree, but keep it cached behind the scenes because you might need it back later.
- Use the `hidden` attribute when you want to remove an element from the page + accessibility tree in a semantic way that doesn't require you to touch any CSS.

## Learn to Appreciate the Trade-Offs

Honestly, I'm a little exhausted just looking back at these tactics. For the longest time, I've made it just fine with setting `display` and `opacity` properties to make elements go away when intended, and overall, everything's been just fine. With that in mind, it might be easy to shrug some of these off and continue on your merry way. And no one would die.

But at the same time, these tools that sometimes _feel_ like virtually the same thing can make a big difference in ways that are often overlooked. I'm sure I've shipped a lot of code that does what's needed on the surface, only to neglect things like performance or accessibility.

So, don't just dismiss these as X different ways to do the same thing. There are serious trade-offs between each of them, and you might be surprised by how they can elevate the performance or accessibility of your applications.