challenges

React Components: Part 6 (Solution)

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

import { Loader } from "lucide-react";
 
function Spinner({}: SpinnerProps) {
  return (
    <div className={styles.wrapper}>
      <Loader size={48} />
    </div>
  );
};

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

import VisuallyHidden from "../VisuallyHidden";
 
function Spinner() {
  return (
    <div className={styles.wrapper}>
      <VisuallyHidden>Loading...</VisuallyHidden>
      <Loader size={48} />
    </div>
  );
};

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

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

We’ve named this animation spin to identify it when we want to use it. Here’s how it’s used:

Spinner.module.css

.loader {
  animation-name: spin;
  animation-duration: 5000ms;
  /* let the animation run over and over forever*/
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}

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

function Spinner() {
  return (
    <div className={styles.wrapper}>
      <VisuallyHidden>Loading...</VisuallyHidden>
      <Loader size={48} class={styles.loader} />
    </div>
  );
};

View in UI

Let’s take a look at the Spinner in action:

page.tsx

import Spinner from "@/components/Spinner";
 
export default function Home {
  return (
    <main>
      <Spinner />
    </main>
  );
}

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

.loader {
  animation: spin 5000ms infinite linear;
}

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

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

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

@keyframes pulse {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

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

@keyframes pulse {
  50% {
    opacity: 0;
  }
}

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

.loader {
  animation: pulse 5000ms infinite ease-in-out;
}

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

@media (prefers-reduced-motion: no-preference) {
  .loader {
    animation: spin 5000ms infinite linear;
  }
}

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 .