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:
- Hide it completely
- Hide it visually
- 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:
Snook@snookca@zellwk While it pulls it out of flow, it can obscure clickable items. You could add `pointer-events: none;` to it. I don't know how screenreaders behave with pointer-events turned off; I haven't tested it.
codepen.io/snookca/pen/ZZâŚ22:01 PM - 23 Apr 2019
He also wondered if pointer-events: none
would stop keyboard-trigged click events (which are absolutely essential for screen readers and keyboard users).
Snook@snookca@zellwk Pointer events shouldnât obscure because any click/touch events pass through. Although, actually, unsure if that breaks keyboard-triggered clock events. đ¤00:18 AM - 24 Apr 2019
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:
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.
Vadim Makeev@pepelsbey_07:17 AM - 24 Apr 2019
patrick h. lauke@patrick_h_lauke@pepelsbey_ @zellwk @snookca don't have test results to hand/time to test just now, but in short: yes, in at least some browser/screen reader combination, opacity below a certain value results in content not being announced. suggest sticking with tried and tested (albeit a bit lengthy looking) sr-only styles07:38 AM - 24 Apr 2019
But Jonathan found some research that suggests that opacity
is okay. Patrick further did some tests and agreed that opacity
is okay.
Snook@snookca@patrick_h_lauke @pepelsbey_ @zellwk Whatâs funny is finding a thread from you, Patrick, from 2016 where you said similar. You think itâs a problem but you didnât have a test suite. In further research, I came across webaim.org/blog/screen-re⌠that indicates that opacity should be fine.10:17 AM - 24 Apr 2019
patrick h. lauke@patrick_h_lauke@snookca @pepelsbey_ @zellwk embarassing, but i stand corrected. it appears the half-remembered fact about opacity not working was, in essence, for the opposites case (problems with sites using it thinking it hides it from AT, but it not doing it).
just opacity itself seems to be ignored by AT 1/10:37 AM - 24 Apr 2019
Scott O'Hara also chimed in on the original problem with opacity
@patrick_h_lauke @snookca @pepelsbey_ @zellwk ChromeVox used to completely ignore opacity: 0; essentially treated it the same as display: none.
that was a few years back with the browser extension version. Would need an actual chromebook to test if that's still an issue with the modern build.13:39 PM - 24 Apr 2019
The verdict at this point:
- Opacity seems to be screen-reader friendly!
- 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.
@zellwk Definitely a contextually appropriate solution to hide static text content. But this should be noted as not recommended for visually hiding interactive elements. For instance IOS VoiceOver will not be able to activate a pointer-events none button.04:37 AM - 24 Apr 2019
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.
@zellwk it should not be used on buttons, as visually hidden buttons also won't work correctly with iOS VO. It also has the potential to trigger desktop VoiceOver's reading out of order, as @letrastudio mentioned, depending on real world styling of the interactive element it's used w/in11:44 AM - 24 Apr 2019
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.
@zellwk Definitely a contextually appropriate solution to hide static text content. But this should be noted as not recommended for visually hiding interactive elements. For instance IOS VoiceOver will not be able to activate a pointer-events none button.04:37 AM - 24 Apr 2019
Letra Studio@letrastudio@scottohara @zellwk Even for static text, VoiceOver can sometimes read hidden content out of order (it tries to follow visual order instead of source order). And sadly this solution isn't impervious to that issue. Here's a test: codepen.io/letrastudio/peâŚ08:37 AM - 24 Apr 2019
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!
Letra Studio@letrastudio@zellwk Huh, thatâs weird. Iâve done some more tests, macOS 10.14.4:
- Latest Safari and Chrome act the same: fail example 1, correct on 2
- Firefox reads 1 correctly and fails on 2!
- Safari Tech Preview reads both correctly
Thatâs what I call *finicky*09:19 AM - 24 Apr 2019
Scott O'Hara shared a little more info on when this bug occurs:
@letrastudio @zellwk it's definitely still a bug, as Letra mentioned.
It only occurs if you place position absolute content (visually hidden) within focusable elements.
Spent way too long trying to tackle that when Joe reported it. His last workaround ended up being the best.10:58 AM - 24 Apr 2019
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.
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.
@zellwk @patrick_h_lauke @snookca @pepelsbey_ and that was all just VoiceOver related. nothing in there about how position: absolute can make for awkward announcements when used within interactive elements with PC screen readers...
moral of all of this, there is presently no silver bullet to be found here.14:09 PM - 24 Apr 2019
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! đą).
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).
@snookca @zellwk @patrick_h_lauke @pepelsbey_ I look at it like float vs inline-block vs column-count vs flexbox vs grid vs positioning for layout purposes.
or display none, visibility hidden, inert, aria-hidden=true, role=presentation, and the hidden attribute for fully hiding content
they are all contextually appropriate14:19 PM - 24 Apr 2019
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.
Snook@snookca@scottohara @zellwk @patrick_h_lauke @pepelsbey_ Right, thatâs exactly what Iâm saying. Iâd like to (or would like someone to) detail the different approaches and describe what contexts they are appropriate for.14:29 PM - 24 Apr 2019
@snookca @zellwk @patrick_h_lauke @pepelsbey_ that's what i tried to do at a high level in my post about this, while leaving out a lot of the techniques that i knew had problems.
though per all this, seems there's a need to go deeper and call out the various pros/cons.15:04 PM - 24 Apr 2019
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. This article was originally posted on my blog. Sign up for my newsletter if you want more articles to help you become a better frontend developer.