Use the `prefers-reduced-motion` media query to toggle CSS and JavaScript animations

Elijah Manor
InstructorElijah Manor

Share this video with your friends

Send Tweet
Published 2 years ago
Updated 2 years ago

In this lesson we focus on toggling animations on and off using the prefers-reduced-motion media query. The sample web app has 3 types of animations (CSS, SVG SMIL, and JavaScript) and we will toggle these animations using different techniques such as @media, matchMedia, and a custom hook.

Elijah Manor: [0:00] Here we have a small web application with lots of animation. Many operating systems have a reduced-motion feature in their accessibility settings. As of now, our web app does not respect the setting, but we'll update our app to do so.

[0:13] The code for the web app is on the left. I'll keep the app running on the right so we could see updates as we go. First, we'll focus on the CSS. Then, we'll come back to the JavaScript later.

[0:23] Let's go to the very bottom. Thankfully, there's a media query called prefers-reduced-motion that we can detect. It could have values of no-preference and reduce. We'll detect when a user prefers a reduced experience and adjust the CSS accordingly.

[0:39] Let's go find the CSS selector that controls the rotation animation of the logo. OK, that's the App-logo. Let's add a new section for App-logo and turn off the animation. Once we save, you'll see on the right that the rotation of the logo has stopped. It's now respecting the reduce motion operating system setting.

[1:05] Now let's go find a selector that controls the stroke animation for the orbits. It's Logo-orbit. We'll go down and group our previous selector with Logo-orbit so that they both could have animation of none. We'll save our file. It'll update the right. You'll see that the stroke animation now stops as well.

[1:27] The only part of our SVG that's animating are the electrons. Those are not animated with CSS but with Smile. The animation motion element defines how the electrons move along the motion path.

[1:40] Let's grab the Logo-electron class of the circle elements, go back into our CSS file, and add a new rule set that hides the elements with display: none. Although that does remove the animation for the electrons, we should probably pause the animation instead of totally remove those elements.

[2:00] Let's go back and remove the display: none declaration and try another technique. For that, let's turn our attention to the app.js file and toggle the Smile SVG animation via JavaScript. Instead of using CSS to detect reduce motion, we could use the match media API via JavaScript.

[2:20] First, let's create some new state via the useState React hook and call it shouldReduceMotion with a setShouldReduceMotion setter. We'll default the setting defaults and set it later once we know what the operating systems value is. Next, we'll create a useEffect React hook. We'll have it only run after our component first renders.

[2:45] Here, we'll create a new mediaQuery with matchMedia, passing the same criteria that we used over in our CSS file previously.

[2:54] Let's go ahead and set the current state of the query to setShouldReduceMotion. The mediaQuery's match property will be true or false, based on the mediaQuery's string.

[3:06] Now let's add code to listen to changes to the prefers-reduced-motion media query. We'll define a handle media function that will accept changes and update our setShouldReduceMotion setting with e.matches. Then we'll add an EventListener to our media query and listen for any changes that occur, passing our handleMedia function.

[3:29] Finally, we'll return a function to clean up after ourselves, and remove the EventListener from the mediaQuery for the change event.

[3:38] Now that should-reduce-motion is all set up, we should be able to use it to toggle the SVG animation. Let's create a new logoRef with the React.useRef hook, and come down and add our ref to the ReactLogo SVG.

[3:55] Let's create a new useEffect hook that will toggle the SVG's animation. If shouldReduceMotion is true, then it will grab the logoRef's current property, which is the SVG DOM element, and call pauseAnimations(), so that the electrons will stop moving.

[4:12] Otherwise, we'll call the unpauseAnimations() function of the SVG to resume any pause animations. We'll tell useEffect to run this effect any time that the value of shouldReduceMotion changes. Once we save, you could see on the right that our electrons indeed did stop animating.

[4:32] This is one of the rare cases where we may want to use the useLayoutEffect hook since we're working outside of React, but it should work either way.

[4:41] If we open back up our Accessibility settings, we could toggle on and off the Reduce motion option to have our UI immediately update based on CSS and JavaScript media queries.

[4:53] Lastly, we'll focus on removing the animation from our bar charts, which are animated via JavaScript. The code uses framer-motion for the animation. You could see where we're using the animate prop, on lines 81 and 92.

[5:08] Since we already have defined shouldReduceMotion, we could leverage that state and update the animate prop to either be undefined or true, based on its value. Then we'll copy that logic and update the other animate prop as well. Let's save our code and see what happens.

[5:28] If reduce motion is turned off, everything animates. Once reduce motion is enabled, not only does the SVG stop animating, but the Framer Motion bar animations stop as well. Instead of animating, the bars immediately snap to their new values.

[5:45] Before we finish, let's utilize a custom hook that Framer Motion provides for us called useReduceMotion. This essentially does all the stuff that we were mainly writing earlier. We could delete much of our code and replace it with shouldReduceMotion = useReducedMotion.

[6:04] Now we should be able to test our web app again with the same results. There should be no animations when enabled, and the animations should resume if we flip the setting off. And they do.