Ryan KubikBlogTwitterGames

Add a Spoiler Tag with Alpine.js

May 4, 2023

7 min read

I was working on patch notes for my game, Island Maker, and I wanted to put some information behind a spoiler tag.

I didn't want to add in something heavy with a build step. So I thought it would be a good time to try out Alpine.js.

Alpine.js is a lightweight JavaScript framework that describes itself as “jQuery for the modern web.”

#Building a Spoiler

Here's what we're going to build. Click on this to reveal the spoiler!

This is a spoiler!

#Starting CSS & HTML

I used two nested spans to create the spoiler above. Here's the HTML before adding any Alpine code.

<span class="spoiler">
<span>This is a spoiler!</span>
</span>

And here's the CSS I used. While the outer span does not have the .revealed class applied, the inner span will have its visibility hidden. This keeps the text content from being selectable, but preserves the space for the inner span in the document layout.

.spoiler {
background-color: #000000;
cursor: pointer;
}
.spoiler span {
background-color: aliceblue;
}
.spoiler:not(.revealed) span {
visibility: hidden;
}

And here it is rendered! Notice you cannot select the text underneath (I promise it's in there).

This is a spoiler!

#Toggling the revealed class with Alpine

#Including Alpine.js

To get started you can drop their script into the head of your HTML document. They provide a link from the CDN unpkg.

<script src="https://unpkg.com/alpinejs" defer></script>

#Adding Alpine attributes

Here's the HTML from earlier with a few Alpine additions. It makes use of three Alpine.js attributes.

<span
class="spoiler"
x-data="{ open: false }"
x-bind:class="open ? 'revealed' : ''"
x-on:click="open = !open"
>
<span>This is a spoiler!</span>
</span>

This is a spoiler!

#x-data

This declares our span as an Alpine “component”. It also initializes the spoiler to an open state of false.

#x-bind

We can prefix standard html attributes like class with x-bind: to allow Alpine to control them. Here we’re binding to the class attribute of our span.

The x-data values defined above are provided to bound attributes. The bound attribute's value is set to the return value of the expression. 🤔

With this code, we apply the revealed CSS class when our spoiler is open.

#x-on

This attribute allows us to listen for events on an element. To listen we prefix the event name, in this case click, with x-on:.

Again, the Alpine component’s x-data values are provided to this code. The values we change here are reflected in the component's state.

When the span is clicked, we are toggling the value of open between true and false.

#Accessibility

There's no native spoiler tag to use in the DOM, so I'm not 100% how to handle accessibility here. Since a spoiler tag functions a bit like a button, I think these attribute are a reasonable start:

  • role="button"
  • tabindex="0"
  • aria-label="reveal spoiler"

#The final spoiler tag

<span
class="spoiler"
x-data="{ open: false }"
x-bind:class="open ? 'revealed' : ''"
x-on:click="open = !open"
role="button"
tabindex="0"
aria-label="reveal spoiler"
>
<span>Spoilered content!</span>
</span>

Spoilered content!

I enjoyed getting functionality like this into my patch notes without introducing a build process. Alpine.js seems pretty powerful and convenient to drop into an existing page.

The one drawback I ran into was the repetition of code. It got a bit exhausting to copy all these attributes to each spoiler I wanted to make. Then, if I needed to change something I'd have to make sure I made the same change in each location. I think this would prevent me from choosing Alpine on a larger project.