DEV Community

Cover image for CSS Shorts: Spoilers and Hidden Content
CodeDraken for Learn2Dev

Posted on • Originally published at learn2dev.com

CSS Shorts: Spoilers and Hidden Content

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;
}
Enter fullscreen mode Exit fullscreen mode

CSS Spoiler

image-20201215174322282

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

image-20201215174203729

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>&lt;summary&gt;</code> tag.
</details>
Enter fullscreen mode Exit fullscreen mode

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

image-20201215174040124

<details class="spoiler-details">
  <summary>Answer Key</summary>
  <p>This is a styled <code>&lt;details&gt;</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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 });

Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

https://codepen.io/codedraken/pen/gOwwbjQ

Top comments (3)

Collapse
 
milahu profile image
milahu • Edited

for a [Spoiler] paragraph that opens to [Spoiler: spoiler text] see also Use details and summary tags as collapsible inline elements

Collapse
 
maksimepikhin profile image
Maksim N Epikhin (PIKHTA)

Cool! It is better to remove the selection with a black border after clicking on the spoiler.

Collapse
 
codedraken profile image
CodeDraken

Ah, I didn't think about that... good catch!