You can find a summary of the code updates in this pull request . Read on for explanations.
1. Lucide React
The Lucide React components aren’t that well documented, unfortunately. Usually, you can find the icon you’re looking for, and use upper camel case capitalization for the component. Here, the icon we want is loader , which is the Loader
component in Lucide React. We’ll have to import Loader
and use the size
prop to meet the size spec of 48px
.
Spinner.tsx
2. Screen reader text
This app already contains a VisuallyHidden
component, which is based on Josh W. Comeau’s VisuallyHidden component (with the ‘press key to show text’ functionality omitted.)
This component exposes its children to screen readers, but uses CSS to ensure the children are not visible on visual screens. We can add a VisuallyHidden
component to the Spinner
component div
to meet the spec for screen reader text:
Spinner.tsx
3. Spin animation
We’ll need keyframe animations to get our Spinner to spin. Keyframe animations specify the CSS rules at various points during the animation cycle, and the browser adds the animation between these states. Here we can use from
and to
to specify that the icon should rotate from 0 degrees to 360 degrees (one full rotation).
Spinner.module.css
We’ve named this animation spin
to identify it when we want to use it. Here’s how it’s used:
Spinner.module.css
You can read more about these properties in MDN .
Then we can apply the styles by adding the styles.loader
class to Loader
in Spinner.tsx:
Spinner.tsx
View in UI
Let’s take a look at the Spinner in action:
page.tsx
Looking pretty good! But we can make the code simpler.
Combining animation rules
There’s a shorthand for the animation CSS properties , where we can put all the properties on one line. Here’s what that looks like:
Spinner.module.css
Simplifying keyframes
There’s also a short-cut in keyframes: if the beginning or the end of the animation is the default state, then we can omit it from the keyframes declaration . In this case, the default state is transform: rotate(0deg)
, so we can remove the from
specification.
Spinner.module.css
These code simplifications aren’t necessary; if you’re just starting out with keyframes animation, it might be easier to leave the code more explicit for now.
4. Fade animation
All right, on to the final point of the spec: if the user has set ‘prefers-reduced-motion’ to reduce
, then we should reduce the motion by fading in and out rather than rotating. This calls for a new keyframes declaration.
Spinner.module.css
As before, we can eliminate any states at the beginning or end that are the default state. In this case, the default state is opacity: 1
, so we can slim the declaration down to this:
Spinner.module.css
In other words, start out at the default state (opacity: 1
), then halfway through, end up at opacity: 0
(totally faded out), and then end up back at the default state.
In the next step, we’ll use a different animation depending on the prefers-reduce-motion
value. For now, let’s update the .loader
class to fade instead of rotate, so we can see what this looks like.
Note: ease-in-out
is the animation-timing-function which makes the fade in / fade out feel a little more natural. If you’ve never played with easing functions, try replacing ease-in-out
with linear
, ease-in
or ease-out
to see how the animation changes.
Spinner.module.css
All right, this is looking good too.
5. prefers-reduced-motion
The final step: set up the CSS so that uses who prefer reduced motion see the fade, and those who have no preference see the rotation. For that, we need a a prefers-reduced-motion media query . We can leave the default animation for .loader
as pulse
(like it is now), and add the spin
animation for folks who have prefers-reduced-motion
set to no-preference
.
Spinner.module.css
6. Cleanup?
We’re left with a couple artifacts from new-component
that we don’t really need: the SpinnerProps
type declaration, and the styles.wrapper
CSS class for the enclosing div
. I chose to leave these in, in case they’re needed somewhere down the road. You can remove them to keep the code cleaner if you wish.
Up next
This concludes the React Components series! We’re in pretty good shape to start the Next.js data fetching app in the next series .
Please consider supporting my work!
Here are a few things that would really help:Let your network know about Hands-on Web Dev Challenges by posting a link and tagging me on LinkedIn or Twitter
Invite your friends to subscribe to the free Hands-on Web Dev Challenges newsletter
Tell me about your ideas for new workshops, by replying to a Substack email or posting on LinkedIn or Twitter
Become a paid subscriber to the newsletter
Workshops in this series
- Setup: files and shadows
- Props and TypeScript
- Delegated props
- Variants: Button component
- Centering: Card component
- Animations: loading spinner