On the web, we have two semantic tags we can use when we want to indicate that part of a sentence is particularly significant:
<strong>element is meant for things “of great seriousness or urgency”, like warnings, according to MDN.
<em>element is meant to indicate verbal stress, so that your internal narrator can accurately reproduce the message and infer the correct meaning. For example: “Why would Jerry bring anything?”, and “Why would Jerry bring anything?”
These two elements are often used together to emphasize dire warnings or critically serious situations: Do not open the window on the space ship, you will be sucked into space.
But what about when we want to emphasize something positive? In our non-digital lives, we can use tone, timing, and body language to express all kinds of emotions. It kinda sucks that our only tools on the web are associated with stressful and serious situations.
For my blog, I want more expressiveness than these tags alone can offer. So I'm working on some new ones. I started with this spicy cursive variant, and I recently added a second: .
The sparkles indicate that something is new and shiny, or something that has captured my affection. It's meant to increase prominence, but in a positive way!
We can use it on more than just text, as well. Here are some examples:
Today we're going to explore how this was built, so that you can have sparkly text in your project as well!
Link to this headingPlanning it out
From an interface perspective, I imagine it working like this:
Each sparkle will be its own HTML element, a
span. We'll have some sort of loop that adds a couple elements a second. Each element will use keyframe animations to twinkle: a combination of scaling and rotating.
If we're not careful, we'll wind up polluting the DOM with a bunch of stale sparkle elements, so we'll also need to clean up after ourselves; we'll do periodic garbage collection, and remove nodes that have finished twinkling.
Finally, we'll position each sparkle randomly within the box formed by the child element.
Each sparkle should be somewhat unique as well, to keep things interesting.
With this game plan in mind, let's start building! First, we need a sparkle asset.
Link to this headingCreating an asset
In this tutorial, we'll use the following asset:
It's an SVG, not a JPG or PNG. We want it to be an SVG so that we can dynamically alter it: by using inline SVG elements, we can change the
fill color using JS!
There are many ways to get an appropriate SVG:
- Search the Noun Project for one.
- Download the one I created (right-click and “Save As…”). It's released under Creative Commons Zero.
- Make your own!
I use Figma for all of my illustration needs. It's an incredible, free piece of software. It's cross-platform, and comes in desktop and web-app varieties (it's even built with React!).
I recorded a 1-minute tutorial on how to use Figma to create a sparkle SVG:
Link to this headingGenerating Sparkles
We need a function that will create a new “sparkle” instance.
Each sparkle should have an ID, a random size, and a random position. Here's a first pass:
We can call this function whenever we need a new sparkle, and it'll have a random position. We use percentages for layout since we don't actually know the width and height of our container.
We'll create a new
SparkleInstance component, which will consume some of this data to render a sparkle.
Earlier, we created an illustration in Figma:
We can export this as an SVG, and wind up with something that looks like this:
The nice thing about SVGs is that they're already almost JSX! We can use a nifty tool like svg2jsx to tweak the handful of small details that need to change. We'll use this SVG as the basis for a new React component,
Every sparkle instance will have its own color, size, and position, so these become props for our new component. Previously-fixed values in our SVG become dynamic, powered by props.
I've wrapped the
svg in a styled-component,
Svg. This lets us add some baseline styles for our sparkle.
Let's create a React component, and start by rendering a single sparkle. We'll sit it next to whatever
children we've passed it:
- We've created a single
SparkleInstancewith a random size and position.
- We've given it a z-index of 2.
childrenare wrapped in a
ChildWrapper, which is a
strongtag with a z-index of 1.
- Both of these elements are wrapped within a
With this done, we have a sparkle being generated haphazardly above our wrapped element! Click/tap the button to generate a random new sparkle:
We have two steps remaining before we have a working prototype:
- Add the animation, so that each sparkle appears to twinkle.
- Periodically generate and clean up sparkles.
Link to this headingTwinkling animation
We want our sparkles to change in two ways:
- It should rotate, relatively slowly
- It should grow and shrink
transform property can help us with both of these goals. As a first stab, we can do something like this:
Our animation starts at
scale(0), which means it's shrunk down to the point that it's invisible (0x its normal size). At the 50% mark, we've grown to its full size (1x), and rotated it 90 degrees. By the time the animation completes, we've rotated another 90 degrees, and shrunk back down to 0x size.
Let's see what this animation looks like. Click the trigger to generate a sparkle (I've blown it up so that we can see the effect clearly).
🤔 This isn't super twinkly, is it? I see two problems:
- Each step is eased, so you wind up with a jerky 2-step animation; it sorta pauses in the middle, since it's easing to the 50% keyframe.
- There are two properties being tweened—rotation and scale—and they're happening in total lockstep. I want these properties to be handled separately, so that their timing and easing can be independently controlled.
The first thing we need to do is separate out the animations: I want to be able to control the scale and rotation separately. In order for this to work, I need a wrapping div: you can put multiple keyframe animations on an element, but not if they both modify the same property. In our case, both keyframes tweak the
Instead of a single keyframe on the SVG, let's add a second keyframe to a parent element. We'll tweak the easings so that our rotation is linear, while our scaling parent has a symmetrical ease:
With this split, things are looking a lot smoother:
Link to this headingGeneration and Cleanup
One final task lays in our path: Dynamically generating a bunch of sparkles, and cleaning them up after they've finished twinkling.
My first instinct was to reach for
setInterval. This method lets you schedule updates in an asynchronous loop. We can add 1 new sparkle every 500ms, for example.
The problem with this approach is that it feels super robotic/synthetic. I wanted something that felt more organic and haphazard. I didn't want such a staccato rhythm of new sparkles!
I created a new hook,
useRandomInterval. It works like
setInterval, except you pass it two numbers, a min and a max. For each iteration, it picks a random number in that range. This leads to a much more natural effect. Here's a side-by-side comparison, each generating an average of 2 sparkles a second:
The beauty of custom hooks is that they can totally abstract a lot of complex stuff. I've published this
useRandomInterval hook as a snippet. If you're curious, you can read about how it works, but don't feel obligated; feel free to copy/paste it, and use it as you'd use
Inside our interval, we'll do two things:
- Generate a new sparkle.
- Clean up any old sparkles.
Here's what that looks like:
Link to this headingAccessibility
Whimsical features like sparkly text are great, but it's important that they don't come at the expense of accessibility.
In Accessible Animations in React, we looked at how the "prefers reduced motion" media query allows people to indicate that they don't want to see any animations. The usePrefersReducedMotion hook lets us access that value from within JS.
In this case, I want to do two things:
- Disable the "twinkling" animation.
- Disable the random interval that adds them and cleans them up.
If the person prefers reduced motion, we can generate 3-4 sparkles and present them statically:
We'll initialize our
sparkles state with this set of sparkles, and disable our
useRandomInterval loop if motion is disabled:
We can disable the loop entirely by passing
null as the min/max times. Happily, this hook is fully responsive, meaning that the user can toggle their "prefers reduced motion" status on and off, and our sparkles will freeze as-needed.
One last step—we need to disable both animations when the media query is matched, in CSS:
Link to this headingPulling it all together
Here's the final version of the code we've built:
In order for this to work, you'll need a few dependencies:
- a random utility function
- a range utility function
- the usePrefersReducedMotion hook
- the useRandomInterval hook
Link to this headingJust the beginning
<Sparkles> component is sort of like an MVP; it does the job, but there's a lot of room for improvement.
On this blog, I've taken the liberty of making a few other changes:
- Sparkles can appear either in front of or behind the children
- Sparkle positioning isn't quite random, I try and pick nice arrangements
- You can click sparkly text to disable the effect
- Sparkles are only generated when the element's on-screen, using the IntersectionObserver API.
The code snippet above is meant to serve as a starting point for your own tweaks and customizations. A big part of what makes this effect delightful is that it's . Be creative, and add your own tweaks to it!
Instead of making an opaque NPM package, we've constructed this effect from scratch. Now it's your turn to build on top of it, and add your own unique touch.
I can't wait to see what you come up with!
May 19th, 2020