Back to All Posts

There Are a Lot of Ways to Hide Stuff in the Browser

There are a surprising number of ways to hide elements in the browser, each with their own trade-offs and advantages. Let's explore some of the classics, as well as a few modern approaches.

I stumbled across a pull request in Marc Grabanski's modern-todomvc-vanillajs 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 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

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

From an accessibility standpoint, hiding an element like this entirely removes it from the accessibility tree, 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

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'

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, 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 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.

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

And second, using content-visibility:

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. 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, and the more modern "snapshot" documents maintain the same sentiment. 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

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 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.


Alex MacArthur is a software engineer working for Dave Ramsey in Nashville-ish, TN.
Soli Deo gloria.

Get irregular emails about new posts or projects.

No spam. Unsubscribe whenever.
Leave a Free Comment

0 comments