A new (and easy) way to hide content accessibly

When I want to hide content accessibly, I always turn to Jonathan Snook’s snippet.

.element-invisible {
  position: absolute !important;
  height: 1px; width: 1px;
  overflow: hidden;
  clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
  clip: rect(1px, 1px, 1px, 1px);
}

But yesterday, I happened to chance upon Scott O’Hara’s article on hiding content. Scott says we only want to hide content in three different contexts:

  1. Hide it completely
  2. Hide it visually
  3. Hide it from screen readers

When we say hide content accessibly, we effectively mean option #2 (hiding content visually, but not from screen readers and keyboard users).

Then I had an idea

If we only want to hide elements visually, why don’t we use opacity: 0? Opacity is used to hide elements visually anyway. Content hidden with opacity: 0 is still accessible to screen readers.

.hide-accessibly {
  opacity: 0;
}

I took it up a notch by adding position: absolute. This takes the element away from the document flow; and allows us to style other elements as if the hidden content isn’t there.

.hide-accessibly {
  position: absolute !important;
  opacity: 0;
}

I thought this felt good enough, and I asked Jonathan about it.

Here’s what he responded with:

He also wondered if pointer-events: none would stop keyboard-trigged click events (which are absolutely essential for screen readers and keyboard users).

I was curious, so I tested pointer-events: none and discovered it works with keyboard-generated clicks, screen-reader-generated clicks, and JavaScript generated clicks.

Here’s the Codepen I used for my test:

See the Pen Pointer-events test by Zell Liew (@zellwk) on CodePen.

I reported my findings back to Jonathan and he said we might have a winner!

The snippet

Here’s the snippet if you want to use this method.

.hide-accessibly {
  position: absolute !important;
  opacity: 0;
  pointer-events: none;
}

DISCLAIMER: This method is still incredibly new. I’ve only tested it on the latest versions of Firefox, Safari, and Chrome. I wasn’t able to run a test on Edge and other browsers yet.

If you’re an accessibility consultant, I’d greatly appreciate it if help me take this snippet out for a spin.

For the rest: I don’t recommend using this snippet in production yet. (Not until I get confirmation from accessibility experts).

UPDATE: Many developers voiced their opinions, concerns, and experiments over at Twitter. I wanted to share with you what I consolidated and learned.

At the start, all three properties were debated upon.

First, let’s talk about opacity.

The problem with opacity?

Patrick and Vadim were concerned about opacity because it seemed to break in some browser/screen reader combination.

But Jonathan found some research that suggests that opacity is okay. Patrick further did some tests and agreed that opacity is okay.

Scott O’Hara also chimed in on the original problem with opacity

The verdict at this point:

  1. Opacity seems to be screen-reader friendly!
  2. But it might not work on ChromeVox now. More tests are required to validate this.

Next, let’s talk about pointer-events because it’s the second most-troublesome thing.

Pointer-events

Scott O’Hara pointed out that iOS Voiceover users wouldn’t be able to trigger a click if an element had pointer-events: none. I tested what Scott said and found it to be true.

This means we can’t use the pointer-events universally on all elements.

My next question was: If we can’t use pointer-events, what if we set z-index to -999? This would prevent the hidden element from obscuring clickable elements.

.hide-accessibly {
  position: absolute !important;
  opacity: 0;
  z-index: -999;
}

Well, Scott said we shouldn’t use z-index: -999 on buttons as well, because visually hidden buttons wouldn’t work correctly on iOS Voiceover.

I’ll be honest. I don’t understand why z-index: -999 wouldn’t work correctly with iOS Voiceover, so I don’t have a proper conclusion here. I didn’t test it.

MacOS Voiceover reading content out of source order

Scott and João Beleza Freire (@letrastudio mentioned above) pointed out a noteworthy bug where macOS Voiceover read content out of source-order.

I did my own test on this, but the bug Joao reported doesn’t seem to happen on my computer, even though we used the same device!

Scott O’Hara shared a little more info on when this bug occurs:

It turns out, a bunch of experts (including Scott) were already going back-and-forth about this macOS Voiceover bug since 2017. It’s worth reading through the entire issue thread about the problem.

From what I’ve read, it seems like the problem happens when position: absolute is used. When you use position: absolute and you mess around with the CSS positing, it messes with the position of the Voiceover focus-ring, which changes the reading order.

An image detailing the experiments done by Joe Watkin on how CSS affects focus rings

This means ANY solution that there’s a chance that macOS Voiceover screws ANY solution that contains position: absolute.

😱

And this whole issue is only Voiceover related. We haven’t considered how position: absolute can make it weird for other screen readers.

The solution in HTML Boilerplate

Some folks have suggested they use the sr-only snippet from HTML5 Boilerplate. They felt it’s the best method out there because many experts came together to create this.

.sr-only {
  border: 0;
  clip: rect(0, 0, 0, 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  white-space: nowrap;
  width: 1px;
  /* 1 */
}

However, this is the same solution that triggered the issue thread I mentioned above! Experts, like Scott O’Hara, have been working on this since 2017 and there doesn’t seem like THE solution to date.

The best solution so far was suggested by Joe Watkin:

.visuallyhidden {
    border: 0;
    clip: rect(0 0 0 0);
    height: auto; /* new - was 1px */
    margin: 0; /* new - was -1px */
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
    white-space: nowrap; /* 1 */
}

At the time of writing, this solution has not been integrated into HTML5 Boilerplate officially. Do take note.

Again, it’s worth going through the conversations in the issue thread if you nerd out in this field. It’s priceless. (As an aside, I learned that aria-label is ignored by Google’s and Microsoft’s translators! 😱).

Update: Aswin notified me that clip is deprecated. We should use clip-path instead. I haven’t tested clip-path in production yet though.

Concluding words

While Joe Watkin’s solution seems to be the best so far, the real answer is it depends. Each method we discussed above, in Jonathan’s article, and elsewhere on the internet has their pros and cons.

Like Scott mentioned, it’s almost like a situation where you can choose between grid vs flex vs other layout methods. You have to pick the best method depending on the situation (and your knowledge of the weird quirks).

There’s one thing we can do to further clarify things. And that’s to compile the pros and cons of each solution we know so far.

Unfortunately, this is something that’s way out of my league right now. If you’d like to step up and participate in the conversation, I’m sure Jonathan, Scott, and many others would love to chat!

Thanks for reading. Did this article help you out? If it did, I hope you consider sharing it. You might help someone else out. Thanks so much!

Comments are closed

Please contact me if you want to talk to me about this article.

If you spot a typo, I’d appreciate if you can correct this page on Github. Thank you!

Hold on while i sign you up…

🤗
Woohoo! You’re in!
Now, hold on while I redirect you.