The topic of how to make an accessible spoiler component, or āshould there be a spoiler element?ā has recently (re)-caught my attention. (I wrote most of this back in February and then forgot about itā¦ as I seem to do these days. But then people necroād a GitHub issue on the topic, so here we go again)
Now, I could do what I usually do and write some long winded junk about what a spoiler is, how itās essentially a glorified disclosure widgetā¦ maybe mention something about the details
and summary
elements and how they likely wouldnāt be sufficient for all use cases. Blah blah blah. Boring.
Instead, Iām just going to tell you what Iād expect from a spoiler component if someone were to build one, or if one were to ever be standardized. Iāll try to keep it as short as I possibly can (which Iām bad at).
Spoiler 1: there needs to be a semantic container element
While one obviously needs some sort of container to delineate the content that needs to be marked as spoilery - it canāt just be a plain old div
. Insert your bewildered āgaspsā here.
Someone using a screen reader, for instance, needs to be able to know that theyāve entered a section of content that represents spoiler material, and also importantly, when theyāve reached the end of such content.
We should also be mindful to the fact that spoilers can represent an entire section of content, which could contain multiple paragraphs / structured content (e.g., lists nā such). Or, it could be a string of text within a paragraph.
In lieu of a standardized element with a transparent content model (see how del
and ins
work / allow for otherwise āinvalidā nesting, if you arenāt sure what that term means), the spoiler container will need to be a custom element so we donāt need to switch between a div
and a span
(the latter being able to be used within a p
element, where the former would cause the browserās parser to kick it out).
<spoiler-component>
...
</spoiler-component>
As far as default accessibility semantics are concerned, a custom element is a long-winded way to write span
or div
, and that wonāt cut it. So we need to give this thing a role and an accessible name to help define those boundaries I mentioned.
<spoiler-component aria-label="Spoiler" role="group">
...
</spoiler-component>
With this in place, we now have a wrapper to define the boundaries of the spoiler content. It will be exposed as a āgroupā with the name of āspoilerā, identifying that any content within this group is spoiler-related. (please note that group
might not be the ābestā role for thisā¦ but also, in my opinion there is presently no better role, so here we are)
Spoiler 2: there needs to be a way for it to be accessed by non-pointer (mouse) inputs
If you think that someone should be able to reveal the spoiler content by clicking anywhere on this element, then sure. That can happen. But, you canāt stop thinking there - sorry, I know thinking is hard.
We need a way for someone who isnāt using a pointer device (mouse, touch, etc.) to be able to reveal the spoiler content. So what are we to do? We add a keyboard accessible control to provide this function. You might have heard of ābuttonsā?
<spoiler-component aria-label="Spoiler" role="group">
<button>Reveal Spoiler</button>
...
</spoiler-component>
Now we have a keyboard accessible way to reach this spoiler content. But also the visible button text helps identify why this blurred or blocked out content is presented this way. Cause you knowā¦ spoilery, content or āgated/potentially nsfwā content can look similar in their visual treatment (visually obscured but still takes up space in the document flow). But they actually represent very different thingsā¦ almost like just because some things look similar, you canāt just assume theyāre the sameā¦ if this was Twitter, Iād be subtweeting really hard right now.
While weāre at it though, it should be a default that someone can show/hide (a.k.a., ātoggleā - itās an industry term) the spoiler content. And thatās for a few reasons:
- It might not be desired for the content to remain visible. We donāt know what the spoiler content could be, and maybe thereās something that gets displayed that one doesnāt always want to be displayed? Use your imaginationā¦ People should have a way to hide this content again if they want/need to.
- If the button goes away when the spoiler content is shown, then we have to worry about focus management. Otherwise, as often happens when people donāt think about this, a screen readerās virtual cursor could find itself sent back to the start of the web page, and that user would then need to re-navigate the page to find the content again - but this time without the ability to navigate to the button with the name of āspoilerā.
- Keeping the button in place also allows us to provide a confirmation of action to the user by using an
aria-expanded
attribute. When first encountering the button, someone using a screen reader will hear the button is in the ācollapsedā state. And activating it will announce it in an āexpandedā state, thus signaling the button actually did what it was supposed to do, and now they can go spoil themselves.
<spoiler-component aria-label="Spoiler" role="group">
<button aria-expanded="true|false">Reveal Spoiler</button>
...
</spoiler-component>
Spoiler 3: gotta actually hide the spoiler content to everyone
We canāt just blur, block or blot out the spoiler content (I donāt actually know what a blotting treatment would visually entail here, but it was a 3rd ābā word for obscuring content, so Iām keeping it there. Why am I explaining this to you?).
There are two bits we need to consider here:
- Visual styles alone wonāt make the content actually hidden. Someone using their mouse could still copy the visually obscured content, or someone using a screen reader could still access the content. Additionally, if there are nested keyboard focusable elements within the spoiler, then those too could still be reached by someone navigating by keyboard. None of thatās what we want.
- Since the āSpoilerā toggle button needs to be inside of the āSpoilerā
group
, we canāt blanket obscure everything within it.
So we can adjust the markup by adding a nested container as a sibling for the button. We can also use the inert
attribute to serve as our styling hook, which also makes sure the content canāt be accessed when its obscured.
<spoiler-component aria-label="Spoiler" role="group">
<button aria-expanded="false">Reveal Spoiler</button>
<spoiler-content inert> <!-- look, it's inert! -->
...
</spoiler-component>
</spoiler-content>
One problem with this idea right now is that inert
content can still be found when using a browser's "find" feature (e.g., ctrl + F). My feelings on whether inert content should be 'found' are complicated... But I'm trying to keep this short. So moving on.
Spoiler 4: But howzitwork?
Well, I made a fancy-dancy (it is neither of those) CodePen to show you. The JavaScript is slapdash, the CSS is incredibly basic. Iām sure you can do better. Donāt @ me. I donāt care.
See the Pen Spoiler Component by Scott (@scottohara) on CodePen.
But you keep the button visible at all times!? I donāt want that!
Yup. Iām sorry you donāt want that. I donāt want to remove or hide it.
Iām sure a compromise can be made. But the button really should be persistently available so that people can toggle the content, and so that one doesnāt need to worry about moving focus when the button ādisappearsā on them after activating.
Might someone decide to visually hide their button? Sureā¦ Iām not going to fight ya on that, so long as it still works for all your users. But, if one were to standardize this, the default should be as accessible by default as possible. And that probably means the button should be persistent by default.
Wrap it up
A standardized accessible component like this could be useful for more than just temporarily hiding the key plot points of a [ insert thing you want to spoil here ]. Arguably it could be a useful component to have for interfaces that need to edit or present documents where content needs to be obscured.
Additinoally, a standardized component could provide a simplified aural UI in ways that are arguably a bit cumbersome for a web developer to deliver on.
For instance, rather than a group
with the name of āspoilerā - a new variant of a group
role could be created, with a localized role description (a.k.a., the ānameā of the role) of āspoilerā or āobscuredā or something elseā¦ So instead of needing to name a generic group, you could just have an element that was announced with the name of that role.
There are other things that a browser implementation could consider - from exactly how to handle the inert content (making sure it canāt be found by the browser find feature), or even how it might handle the button part / focusability of the element in general. Maybe a browser-implementation could even go to the point of removing the spoiler/redacted content form the source code? I dunno about thatā¦ but you canāt really have truly redacted content on the web if you can just few source to check it out, can you?
My example is merely demonstrating a basic styled example of what Iād expect this to look like based on what can be done as a web author.
Ok, fun. This wasnāt too long, I guess.