If you use Meta’s Threads app on a mobile device, you may have noticed an interesting effect: the spoiler tag that hides text behind a sparkly veil. In this article, I will show you how to implement this effect in the browser using HTML, CSS, and JavaScript.
NOTE: You can scroll to the bottom of this article to find the completed HTML, CSS, and JavaScript code if you don’t want to read the whole article.
The Spoiler Effect on Threads in a Nutshell
If you’re unfamiliar with the “spoiler” tag, it is a Threads feature in which users can selectively hide part of their post until a user is ready to see it. Look at my example below where I am about to post “Highlight the word “hello” to mark as a spoiler!”
Notice how I have highlighted the word “hello” and see a button that says “Mark spoiler.” I click that and then post the thread. See how it looks on my desktop browser:
The word “hello” is now obscured by a gray bar. If someone reading my post wants to see the word I’ve hidden, they will have to click on the gray bar.
The above screenshot is what a reader will see after clicking the gray bar; they can now see the word that I’ve hidden.
In a nutshell, that is how “Mark spoiler” works in Threads; the author selects some text to selectively hide and the user must intentionally perform an action to reveal the hidden text. The intended utility of this feature is for when the user wants to talk about a plot twist or conclusion in a story without giving away–or “spoiling”–the surprise for readers who haven’t yet gotten to that part. It protects readers from passively reading a spoiler by forcing them to deliberately reveal the hidden text.
Note, however, this spoiler bar is a little more fun on the mobile app. The next screenshot shows the same post but from the mobile Threads app:
Notice how instead of a gray bar, it’s now a sparkling obfuscation. How cool! This sparkly spoiler effect is what this article will teach you how to do.
Actual Spoiler Alert!
I am going to be talking about a plot twist from Star Wars: Episode V – The Empire Strikes Back. Even though the movie came out in 1980, it’s possible you haven’t seen it yet and I would hate to ruin the surprise for you. If you plan on watching this movie and have no clue about its plot, this article is definitely going to spoil it for you!
Continue at your own risk!
How to Achieve this Effect
First, we will plan out how to implement this effect before getting into the technical details. Here are the things we need to do:
- Mark text as spoiler
- Create the effect
- Apply that effect to appropriately tagged text
- Remove the effect when a user clicks
Let’s begin!
Mark text as spoiler
First, let’s create an HTML file called “spoiler_text.html” and fill it out with some standard HTML boilerplate code:
<body>
<p>
The biggest plot twist of all time is:
<span class="spoiler">
Darth Vader is Luke Skywalker's father!
</span>
</p>
</body>
This is just a basic HTML page in which we’ve added some text about the biggest plot twist of all time. Right now, the page is pretty bare bones and should look like this in your browser:
Since the background and text in Threads is more of a dark theme, let’s apply that to our HTML page. This isn’t part of the spoilers effect, it’s just something that will make it a little more pleasant to look at later. Let’s create a CSS file called style.css and make the page a little easier to look at. Here’s what I came up with:
:root {
--bg: #0f1115;
--text: #f4f7fb;
}
body {
min-height: 25vh;
display: grid;
place-items: center;
background: var(--bg);
color: var(--text);
}
First, I create a :root rule to store some CSS variables. As of right now, we only have two variables, bg and text, so we could just use the values instead of variables in the body rule. However, later in this project we’re going to have a few more variables and I think it’s best to keep them all in one place, so we’ll start putting our variables in the :root rule early.
Next, I set the min-height, display, and place-items values for the web page. The specific values I’ve added in the above CSS will make the text appear in the middle and slightly below the very top of the browser window. Finally, I use the bg and text variables to set the background and color attributes for the webpage. Now, just below the <title> tag in the HTML file, I add this line:
<link rel="stylesheet" href="style.css" />
And now the browser should look like this:
Feel free to adjust the bg and text variables so that the page you’re looking at is aesthetically pleasing to you.
Now, we need to mark the spoiler text somehow. For now, I’m going to put the spoiler part in a span tag of class “spoiler.” Doing so won’t actually make anything change visually yet, we’re just preemptively marking the spoiler text before creating the effect. The body tag of the HTML file should now look like this:
<body>
<p>
The biggest plot twist of all time is:
<span class="spoiler">
Darth Vader is Luke Skywalker's father!
</span>
</p>
</body>
Now, let’s create the effect!
Create the Effect
To start creating the effect, we’ll take a small step by just writing a little CSS to blur out the tagged text. In our style.css file, we’ll need to make a rule for the spoiler class. We’ll make use of the built-in CSS blur function and make the cursor a pointer. Here’s how my css for the spoiler class looks:
.spoiler {
filter: blur(7px);
cursor: pointer;
position: relative;
}
With this CSS code, we set the filter attribute on the spoiler class to blur(7px) and set the cursor attribute to pointer. As you might guess from the blur function, using it to set a filter value makes the element blurry. The number you pass to the function defines the radius of the blur; the higher the number, the larger the blur effect. We set the cursor attribute to pointer so that when the user hovers their mouse over the effect, the cursor changes to a pointer and lets them know the effect will do something if they click on it.
Additionally, we set the position attribute to relative so that the span doesn’t interfere with any of the other elements in the page
With the new CSS, the page in the browser should now look like this:
The above screenshot shows that our spoiler text is now blurred out thanks to the filter: blur(7px) line in our CSS.
Before we can finally build the sparkle animation and the toggle feature, we have to make a few adjustments to the structure of our HTML. We should actually put two more span elements inside of this spoiler span: one to wrap the content, and one to contain the blur effect. This step will make implementing the toggle effect along with the sparkle animation a little easier.
I will create one span class called spoiler-content, and one called spoiler-blur. The spoiler-content span will wrap the text, while the spoiler-content span won’t wrap anything, it will just sit inside the spoiler span. Here’s what the HTMLin the body tag looks like now:
<p>
The biggest plot twist of all time is:
<span class="spoiler" data-hidden="true">
<span class="spoiler-content">
Darth Vader is Luke Skywalker's father!
</span>
<span class="spoiler-blur" />
</span>
</p>
And the CSS now has to be adjusted as such:
.spoiler {
cursor: pointer;
position: relative;
}
.spoiler[data-hidden="true"] .spoiler-content {
filter: blur(7px);
}
With these updates, I’m making use of the data-hidden attribute by setting it to “true” on the spoiler span and then creating a CSS rule that applies the blur to any spoiler-content span inside of the spoiler span. We will later use this attribute when we create the toggling feature of this effect.
Now, we’re ready to start adding the sparkle effect.
Adding the Sparkle Effect
To add the sparkle effect, we’re going to make use of two things: CSS key frames and some JavaScript. The general strategy will be like this:
- Create a CSS
sparkclass to make each individualsparkle-blurspan covering the spoiler text - Append several
sparkelements as children to thespoiler-blurspan with JavaScript- We will create a function for generating random numbers
- We will set a few custom properties on the element with random values (from the random function mentioned above) that will be used in the CSS animations/keyframes to create the overall “sparkle effect”
- Create a twinkle CSS keyframe that will make the sparks appear to be twinkling
- Create a drift CSS keyframe that will make the sparks appear to be moving
First, we’ll create the spark class with just a few of its necessary properties. I defined mine like this:
.spark {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--spoiler-dot);
}
The position: absolute line will allow each individual sparkle to take up space in its overlay (the spoiler span) without disrupting the position of other elements. The three lines for width, height, and border-radius make the sparkles into perfect circles. If you prefer squares, you can remove the border-radius line, if you like vertical lines, you can play with the width and height properties. The background uses a CSS variable that you can set in the :root rule, I set mine to a white-ish effect like so: --spoiler-dot: rgba(255,255,255,0.85);.
This element defines the style for each individual “sparkle” in our sparkly-spoiler effect. We’ll need to make several sparkle elements of random sizes and append them to random places on the spoiler-blur span element. Create a JavaScript file and call it sparkle.js then import it into the HTML’s body like this:
<script src="sparkle.js"></script>
NOTE: You must put this line inside the body tag of the HTML, it won’t work if you put it in the head tag.
Now, in sparkle.js, we’ll start by making a convenience function for returning random values, since we’ll rely on randomness to get the sparkle effect. I wrote my random function to take a rscale variable and multiply it by a number generated by the Math.random function:
function random(rscale) {
return Math.random() * rscale;
}
Next, we’ll make a function called buildSparkles that takes in a span element, and the number of sparkles we want to create. Each sparkle we create will be created with a starting 2-dimensional coordinate (an x and y representing the left and top properties), an ending 2-dimensional coordinate, a duration of time in which we want the sparkle to move between those two coordinates, a duration of time for which the sparkle should “twinkle”, and a scale variable to control size; all of these values will be randomly created for each sparkle.
Once each sparkle element is built, we’ll append it to spoiler-blur span we passed to the function. Here’s how I wrote my function:
function buildSparkles(spoilerBlurSpan, sparkleCount) {
for (let i = 0; i < sparkleCount; i++) {
const s = document.createElement("span");
s.className = "spark";
const x0 = random(100);
const y0 = random(100);
const x1 = Math.max(0, Math.min(100, x0 + random(18)));
const y1 = Math.max(0, Math.min(100, y0 + random(14)));
s.style.left = `${x0}%`;
s.style.top = `${y0}%`;
s.style.setProperty("--x0", "0px");
s.style.setProperty("--y0", "0px");
s.style.setProperty("--x1", `${x1 - x0}px`);
s.style.setProperty("--y1", `${y1 - y0}px`);
s.style.setProperty("--dur", `${random(60)}s`);
s.style.setProperty("--twinkle", `${random(1.2)}s`);
s.style.setProperty("--scale", `${random(1.5)}`);
spoilerBlurSpan.appendChild(s)
}
}
In the function signature, we take spoilerBlurSpan as the first argument. This will be a span somewhere on the web page that has been marked as containing spoiler text. We also take sparkleCount, which is how many sparkles we want to put on one span. Then, we enter the for loop.
Each iteration of the for loop creates one spark element. The first two lines create a span element and assign it the class name spark.
Next, we set x0, y0, x1, and y1; these variables are basically two sets of random x, y coordinates. Later, we will create a CSS animation called drift that will make the sparkle move between those two coordinates.
The lines setting left and top are putting span elements at the randomly generated x and y coordinates we just generated. The next four setProperty lines are assigning the properties and initial values of x0, y0, x1, and y1 on the sparkle span we just created. The need for these properties will become clear in a bit when we create the CSS animations.
Finally, we use the last three lines to set properties called dur, twinkle, and scale. We have to set these properties and their initial values to be used by the CSS animation we’re about to create. The dur property is going to be used to control the duration of time a sparkle spends moving between its two x,y coordinates. The twinkle property is going to control the “speed” at which each sparkle twinkles (switches between low and high opacity). Finally, the scale property is just going to impact the size of each twinkle.
After setting all these properties, we then use the built-inappendChild method to add the newly created sparkle span to the spoilerBlurSpan we got in the function signature.
Finally, we end the sparkle.js file with a few more lines:
function wireSpoiler(spoilerBlurSpan) {
buildSparkles(spoilerBlurSpan, 75);
}
document.querySelectorAll(".spoiler-blur").forEach(wireSpoiler);
We create one more function called wireSpoiler that takes a spoilerBlurSpan as an argument so that it can send it to the spoilerBlurSpan function along with the number of sparkles to make–in the above example, 75.
Finally, we use querySelectorAll to get all spans on the web page of the class spoiler-blur and pass it to the wireSpoiler method. This ensures that every single spoiler span on our page will get the sparkle-effect, even though we only have one spoiler span on our page.
Now, we move to the CSS.
First, lets create the animations using @keyframes. We’ll create two, drift and twinkle. Add this CSS to the bottom of your style.css file:
@keyframes drift {
from {
transform: translate(var(--x0), var(--y0)) scale(var(--scale));
}
to {
transform: translate(var(--x1), var(--y1)) scale(var(--scale));
}
}
@keyframes twinkle {
from { opacity: 0.25; }
to { opacity: 0.95; }
}
The drift keyframe makes use of the CSS function translate which moves an element to a given location. Since this will be used in an animation property later, we give it a from and to property to define the two positions the element should be moved between. In this case we are using the variables x0 and y0 to define the sparkle‘s starting point, and x1 and y1 as their ending point. Remember, we randomly generated these values in the JavaScript file earlier, so each spark we attach to the spoiler-blur element will have its own starting and ending point.
The twinkle keyframe just defines an opacity value for the spark elements to oscillate between. Opacity is the level of an element’s transparency, so going between a low and high value gives the illusion that the element is twinkling.
Now, we update the spark element’s CSS to include these keyframes as animations. The element’s entire CSS rule should look like this:
.spark {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--spoiler-dot);
animation:
drift var(--dur) linear infinite,
twinkle var(--twinkle) ease-in-out infinite alternate;
}
The change here is the addition of animation to the spark class and adding drift and twinkle. For drift, we pass the randomly-generated dur value we defined in the JavaScript; this value defines how long it takes the spark to move from its starting coordinates to its ending coordinates. We also pass the arguments linear and infinite to the animation, ensuring the movement is in a straight line and repeats as long as the page is loaded.
Similarly, we pass the randomly-generated twinkle variable to the twinkle keyframe animation, this variable defines the amount of time it takes the spark to go from its starting opacity to its ending opacity. The alternate argument ensures the opacity value doesn’t “start over” once it reaches its ending opacity and instead eases back to the starting value.
With this final addition, we should have a nice sparkly veil over our spoiler content. Here’s what mine looks like:
Note that since we are randomly generating a lot of values, your example might not look exactly like mine, and if you refresh the page, it’ll look different again. But as long as you see sparkles twinkling and drifting over your blurred-out spoiler text, you’ve done everything right so far!
The last thing to do is add the toggle effect!
Adding the Toggle Effect
The point of the spoiler tag is to prevent people from seeing something they’re not ready to see. But for users that are willing to risk spoiling a piece of media, we have to add the ability to remove the sparkly veil and reveal the hidden text. We will add that effect now.
The strategy for how we will do this is as follows:
- Add a function that sets the
spoilerspan’sdata-hiddenattribute to the opposite of what it currently is (if it’s true, set it to false and vice versa) - Add this function to the “click” event listener on the
spoilerspan - Update the CSS of attributes inside of a
spoilerspan with adata-hidden="false"attribute
Part one and two are going to happen simultaneously inside of the wireSpoiler function. The first thing we’ll do is get the spoiler-blur element’s parent element (which should be the spoiler span). This is accomplished via this one JavaScript line:
let spoilerSpan = spoilerBlurSpan.parentElement;
Now, just below that line, we’ll create a function called toggle. This function will get the data-hidden attribute of the spoiler and set a variable called hidden to the opposite value. Then, we’ll use the setAttribute method to set data-hidden to the value of the new hidden variable. This is what is meant by “toggling.”
Finally, we’ll use the addEventListener method to add the toggle method to that element’s click event. Altogether, the wireSpoiler function should now look like this:
function wireSpoiler(spoilerBlurSpan) {
buildSparkles(spoilerBlurSpan, 75);
let spoilerSpan = spoilerBlurSpan.parentElement;
function toggle() {
const hidden = spoilerSpan.getAttribute("data-hidden") === "true";
spoilerSpan.setAttribute("data-hidden", String(!hidden))
}
spoilerSpan.addEventListener("click", toggle);
}
In the above code added a few lines to the wireSpoiler function. First, we got the spoilerBlur element’s parent element, which should be the spoiler span. Then, we create a toggle function, the first line of which grabs the data-hidden attribute’s value and checks if it is true. Since we wrote this line in the HTML:
<span class="spoiler" data-hidden="true">
The value will be the string value "true" when the web page initially loads, meaning our hidden variable’s value will be the boolean value true.
On the next line, we set the attribute to the string representation of whatever the opposite of that value is by using !hidden. The ! operator negates whatever variable is next to it. So !true (read “not true”) becomes false and !false (read “not false”) becomes true.
Finally, outside of the toggle function, we use the addEventListener method to add the toggle function to the click event, meaning this function will run any time the spoiler span is clicked.
As of right now, clicking the spoiler tag will just change its data-hidden attribute between true and false. You can view the element in your browser and watch the data-hidden value in the HTML change every time you click it. To actually toggle the blur effect on and off, we need to update the CSS and tell elements inside of spoiler elements with data-hidden attributes of true to behave a certain way.
We do this by making a block of rules for all spark elements inside of a spoiler span whose data-hidden attribute is false. We’ll do this by adding the following to our CSS file:
.spoiler[data-hidden="false"] .spark {
animation-play-state: paused;
display: none;
}
The code above targets only those spark elements inside of a spoiler element with the data-hidden value of "false". Then, we set the animation-play-state to paused, this means the sparkle will stop where it is. We also set the display attribute to none, which essentially hides the sparkle.
Since clicking the spoiler tag will transition its data-hidden value to false if it’s already true, the new rules we wrote above will kick in on all the spark spans, essentially hiding them. It will also unblur the text inside of spoiler-content since we set the filter attribute to blur(7px) only in the rule for a data-hidden="true"element.
With this final update, you should now be able to toggle the blur on and off in the web page! Refresh your browser and click the blurry spoiler text to see the effect in action.
On Your Own
Now that you have the basic ideas down for implementing the spoiler effect, there are lots of things you can do to expand upon and improve the code. For starters, the code above is inefficient because it sets the toggle function on the spoiler span for every sparkle, how would you fix that? You can also experiment with setting individual spoiler tags, each needing their own click to unblur, or set all spoiler tags to unblur when any of them are clicked. There are also tons of things you can do to change the animation style and sparkle appearance. Play around with it!
Conclusion
In this article, you learned how to mimic the spoiler effect from Threads and used HTML, CSS, and a little JavaScript to make it happen. We used keyframes and learned a little bit about CSS animations. You now know the basic strategies behind conditionally hiding and showing text, you can use this effect on your own website for a variety of reasons.
Final Code
At the end of this article, I had three files called spoiler_text.html, style.css, and sparkle.js. Here are the contents of my files:
index.html:
<!doctype html>
<html>
<head>
<title>Sparkly Spoiler Effect</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<p>
The biggest plot twist of all time is:
<span class="spoiler" data-hidden="true">
<span class="spoiler-content">
Darth Vader is Luke Skywalker's father!
</span>
<span class="spoiler-blur" />
</span>
</p>
<script src="sparkle.js"></script>
</body>
</html>
style.css:
:root {
--bg: #0f1115;
--text: #f4f7fb;
--spoiler-dot: rgba(255,255,255,0.85);
}
body {
min-height: 25vh;
display: grid;
place-items: center;
background: var(--bg);
color: var(--text);
}
.spoiler {
cursor: pointer;
position: relative;
}
.spoiler[data-hidden="true"] .spoiler-content {
filter: blur(7px);
}
.spark {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--spoiler-dot);
animation:
drift var(--dur) linear infinite,
twinkle var(--twinkle) ease-in-out infinite alternate;
}
.spoiler[data-hidden="false"] .spark {
animation-play-state: paused;
display: none;
}
@keyframes drift {
from {
transform: translate(var(--x0), var(--y0)) scale(var(--scale));
}
to {
transform: translate(var(--x1), var(--y1)) scale(var(--scale));
}
}
@keyframes twinkle {
from { opacity: 0.25; }
to { opacity: 0.95; }
}
sparkle.js:
function random(rscale) {
return Math.random() * rscale;
}
function buildSparkles(spoilerBlurSpan, sparkleCount) {
for (let i = 0; i < sparkleCount; i++) {
const s = document.createElement("span");
s.className = "spark";
const x0 = random(100);
const y0 = random(100);
const x1 = Math.max(0, Math.min(100, x0 + random(18)));
const y1 = Math.max(0, Math.min(100, y0 + random(14)));
s.style.left = `${x0}%`;
s.style.top = `${y0}%`;
s.style.setProperty("--x0", "0px");
s.style.setProperty("--y0", "0px");
s.style.setProperty("--x1", `${x1 - x0}px`);
s.style.setProperty("--y1", `${y1 - y0}px`);
s.style.setProperty("--dur", `${random(60)}s`);
s.style.setProperty("--twinkle", `${random(1.2)}s`);
s.style.setProperty("--scale", `${random(1.5)}`);
spoilerBlurSpan.appendChild(s)
}
}
function wireSpoiler(spoilerBlurSpan) {
buildSparkles(spoilerBlurSpan, 75);
let spoilerSpan = spoilerBlurSpan.parentElement;
function toggle() {
const hidden = spoilerSpan.getAttribute("data-hidden") === "true";
spoilerSpan.setAttribute("data-hidden", String(!hidden))
}
spoilerSpan.addEventListener("click", toggle);
}
document.querySelectorAll(".spoiler-blur").forEach(wireSpoiler);
Top comments (0)