In this quick tutorial, we'll explore different ways to make spoiler tags that a user can either hover or click on to reveal plot spoiling content.
The Setup
We'll only be using vanilla HTML, CSS, and JavaScript—which I assume you know how to set up already. If not, head over to CodePen and create a new pen. You can also find the completed project and source code there. I have a few options enabled by default on CodePen (SCSS, Babel, Normalize.css) but I don't use any of them in this tutorial. The only initial setup code I added was one line to the CSS to give myself some room.
/* starting CSS */
body {
padding: 1rem 2rem;
}
CSS Spoiler
Using only pure CSS, it's clickable, tabbable, and hoverable. The hovering to reveal is optional, but I recommend keeping it tabbable and clickable for both screen readers and mobile devices.
Code
HTML
<h2>CSS Hover Spoiler / Text Spoiler</h2>
<p>
A pure CSS spoiler revealer that is <span class="spoiler-text" tabindex="0">clickable, tabbable, and hoverable</span>
<br />
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ipsum <span class="spoiler-text" tabindex="0">blanditiis molestiae eligendi</span> non. Ullam doloribus quibusdam at facilis atque! Dolorum praesentium eveniet dicta ullam, aperiam dignissimos voluptate incidunt enim maiores.
</p>
For the HTML, we add placeholder text and to make part of it hidden inside a spoiler we'll want to wrap it in a span with a class of spoiler-text
and importantly tabindex="0"
which is what allows us to tab to it, click it, and style it appropriately.
CSS
.spoiler-text {
background: black;
color: transparent;
cursor: help;
user-select: none;
transition: background 0.3s ease 0.2s, color 0.2s ease 0.25s;
}
.spoiler-text:hover,
.spoiler-text:focus {
background: #e8e8e8;
color: inherit;
}
The background and color properties are self-explanatory, it visibly hides the text. You might think that's all you need, but if someone were to click and drag (select the text) then your plan falls apart because selecting the text reveals it and allows copy/pasting. The next two properties solve this problem.
cursor: help;
changes the cursor from the text select icon to a question mark showing our "black box" does something when clicked or when they move their mouse on it. This is only a stylistic choice and you may want to try cursor: pointer;
instead.
user-select: none;
completely prevents the text from being selected or highlighted, just what we needed. However, this prevents the user from copying the text even after it is revealed.
Moving on to the next part, we have :hover
and :focus
pseudo selectors. The hover happens when you mouse over the spoiler text, and the focus happens when you either click it or "tab" onto it. The focus can only happen if you added the tabindex="0"
in the HTML. Try removing the hover selector to see the difference.
Finally, what we do when a user hovers or "focuses" the spoiler is simple. We remove the black background and change the text color. You could've said color: black;
instead of color: inherit;
but that immediately makes it harder to reuse on say a dark background. inherit
tells the browser to use the same color as the surrounding text. Consider changing the background to inherit
or none
since it's currently hard-coded to that gray color.
One more bit of polishing we can do is smooth the transition between the spoiler being hidden and revealed so it's not instantaneous. This is what the transition: background 0.3s ease 0.2s, color 0.2s ease 0.25s;
is for. It transitions the background color in 0.3 seconds
with a smooth ease
timing function and a 0.2 seconds
delay just to give the user a moment to cancel revealing the spoiler. It also transitions the text color and to get these values you would just try some random values and experiment but usually you'll never go above 0.3s
for transitions.
Pros
- Easy to setup and style
Cons
- Screen Readers might spoil everything
- It's best used for text only
HTML Details Tag
If you want a spoiler that's more like a tab or block of content, then the HTML <details>
tag is an option.
Code
<h2>HTML Details Tag</h2>
<details>
Pure HTML without any Styling, notice how the text defaults to "Details" when we don't provide a <code><summary></code> tag.
</details>
That's all you need for a functional, minimal spoiler using only HTML. More on the <details>
tag here.
Of course we can style it, let's make a styled one and then we'll see what options are available for animating it.
Styled Details
<details class="spoiler-details">
<summary>Answer Key</summary>
<p>This is a styled <code><details></code> tag. Note that the open/close can not be animated/transitioned directly without hardcoding the height for example.</p>
<ol>
<li>A</li>
<li>D</li>
<li>C</li>
<li>B</li>
<li>C</li>
</ol>
</details>
For this one we added a class of spoiler-details
to the <details>
tag and a new <summary>
tag which changes the title from the default "Details" to whatever we put in it.
/* the wrapper/box */
.spoiler-details {
border: 1px solid #bbb;
border-radius: 5px;
padding: 0.5rem;
margin: 0.5rem;
max-width: 50%;
min-width: 300px;
}
/* the title */
.spoiler-details summary {
cursor: pointer;
font-weight: bold;
list-style: none;
padding: 0.25rem;
}
/* the title when the details tag is in the "open" state */
.spoiler-details[open] summary {
border-bottom: 1px solid #bbb;
}
I'm assuming every property under .spoiler-details
is self-explanatory and you can style it however you like (if not, I encourage you to ask questions and discuss in the comments!). There are a few properties that need mentioning for the summary
tag and the [open]
selector.
First, cursor: pointer;
if you followed the previous section for the CSS selector, you might remember that this property changes the cursor to a hand signaling to the user that the element is clickable. The important part to note here is that this is on the summary
element and not the entire <details>
tag because only the title (summary) is clickable.
Next, list-style: none;
this removes the little arrow icon to the left but consider keeping it or adding an icon to make it obvious that it's expandable or clickable.
The <details>
tag comes with an attribute called open
that we can use to change the styles if it's opened or to be used in JavaScript. To select it in CSS we just use a boolean attribute selector by adding [open]
after our class or a details
element selector. Here, we use it to select the <summary>
and add a border-bottom
when it's opened.
Animated Details
Here's a quick example of one way to animate it but I won't go into much detail since animation is a bit out of scope for this tutorial.
<details class="spoiler-details animated">
<summary>Animated Details</summary>
<p>This details block has an animated soft opacity "flash"</p>
<div class="content">
<span>You can also add more intricate animations such as slide-in effects (but you would probably avoid using a border in such cases)</span>
</div>
</details>
The HTML is mostly the same with an added animated
class to the <details>
tag and a content
class for a <div>
that will have a slide-in animation.
/* a soft opacity flash to show the user that something happened */
@keyframes flash {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
/* simple slide in */
@keyframes slide {
0% {
margin-left: -50%;
opacity: 0;
}
100% {
margin-left: inherit;
opacity: 1;
}
}
Here we have two generic animations a flash
that changes the element's opacity from halfway transparent to opaque and a slide
that slides an element in from the left using margin
and fades in at the same time.
We then use these animations once the <details>
is opened.
.spoiler-details.animated[open] {
animation: flash 0.5s ease-in-out;
}
.spoiler-details.animated[open] .content {
opacity: 0;
animation: slide 0.5s 1s ease-in-out;
animation-fill-mode: forwards;
}
We need to add animation-fill-mode: forwards;
for the slide-in animation so that the content stays in the final 100%
position of the slide
animation. We don't need this for the flash
animation because the <details>
is opacity: 1
by default.
Now, your first instinct might be to animate the height
when the <details>
opens/closes but this will not work without hard-coding the height for the details so keep that in mind.
Pros
- Simple and Semantic
- Multiple can be opened at the same time (the pure CSS spoilers can have only one opened at a time)
- Doesn't require anymore work to be accessible
Cons
- Can't animate the open/close transition
- Doesn't work for inline text i.e. hiding part of a paragraph
- Doesn't work on Internet Explorer
JavaScript
For our last spoiler we'll use vanilla JavaScript and add some accessibility features.
Code
<h2>JavaScript Spoiler</h2>
<p>The most flexible option but it requires some more work.</p>
<span class="js-spoiler hidden" aria-label="Spoiler" aria-expanded="false" tabindex="0" role="button">
<span aria-hidden="true">Jar Jar Binks is a sith lord. Clicking this again will toggle the spoiler</span>
</span>
<br />
<span class="js-spoiler hidden" aria-label="Spoiler" aria-expanded="false" tabindex="0" role="button">
<span aria-hidden="true">Wilson doesn't survive... and now you can never close this spoiler</span>
</span>
The HTML is a bit more in depth because we're adding ARIA attributes for accessibility but the main pieces are the js-spoiler
and hidden
classes, and the HTML structure: a <span>
wrapping a <span>
so we have that parent and child relationship.
.js-spoiler {
background: #e8e8e8;
}
.js-spoiler.hidden {
background: black;
cursor: pointer;
border-radius: 3px;
}
.js-spoiler.hidden span {
opacity: 0;
user-select: none;
}
The styling is mostly the same as the CSS spoiler, just style it however you want and hide the text.
JavaScript
The JavaScript isn't too difficult, we just want to listen for any click events on these spoiler tags and toggle the hidden
class along with the ARIA attributes. At this point there's already a design choice to make, do you want the spoiler to be togglable or do you want it to be a click to reveal and then it can't be hidden again (Discord style)?
For this example, I'll write the event handler as if it's togglable but I'll also use an option on the addEventListener
for a one time only spoiler. (this will make more sense in code)
// an array of our js-spoilers
// note that getElementsByClassName() returns a *node list* and not an array
// so if we wanted to loop through the elements to add events we would need to convert it to an array
// that's what the spread syntax [...value] is for, it converts to an array
const jSpoilers = [...document.getElementsByClassName("js-spoiler")];
// normally you would use a loop to add the event listeners
// but we can hardcode it here since it's a tutorial and we have exactly two js spoilers
// a repeatable event listener ("event name", handlerFunction)
jSpoilers[0].addEventListener("click", handleSpoiler);
// passing in an options object with once set to true causes this listener to only happen one time
jSpoilers[1].addEventListener("click", handleSpoiler, { once: true });
That tells the browser to listen for events, now let's create this handleSpoiler
function that will run when the event happens.
function handleSpoiler(evt) {
// this gives us the element we assigned the listener to (the topmost span)
const wrapper = evt.currentTarget;
// toggle the visibility (if the element has the hidden class remove it, otherwise add it)
wrapper.classList.toggle("hidden");
}
That's all we need to toggle our styles but let's not forget about the ARIA attributes. We must grab the inner span, change some attributes, and remove the ARIA label.
function handleSpoiler(evt) {
// outer span (parent)
const wrapper = evt.currentTarget;
// inner span (child)
const content = wrapper.children[0];
// toggle the visibility
wrapper.classList.toggle("hidden");
// set ARIA attributes for screen readers
if (wrapper.classList.contains("hidden")) {
wrapper.setAttribute("aria-expanded", false);
wrapper.setAttribute("role", "button");
wrapper.setAttribute("aria-label", "spoiler");
content.setAttribute("aria-hidden", true);
} else {
wrapper.setAttribute("aria-expanded", true);
wrapper.setAttribute("role", "presentation");
wrapper.removeAttribute("aria-label");
content.setAttribute("aria-hidden", false);
}
}
This part could be cleaned up and improved upon but it's a good starting point for making an accessible spoiler.
Pros
- Most Flexible
Cons
- Requires the user to have JavaScript enabled
<End />
And that concludes this mini tutorial!
Let me know your thoughts, feedback, and share what you've made.
Top comments (3)
for a
[Spoiler]
paragraph that opens to[Spoiler: spoiler text]
see also Use details and summary tags as collapsible inline elementsCool! It is better to remove the selection with a black border after clicking on the spoiler.
Ah, I didn't think about that... good catch!