September 1, 2022 7 min read

Accessible animations in React

Or how not to spin your users round (like a record). Some animations can make users sick. We'll take care of them and make non-essential animations optional.

Last updated: September 1, 2022
Half of a record on white background
Photo by Miguel Á. Padriñán

I like minimalism. I like minimalism in my room, on my screen, and - as you can see - on my blog. But, even for me, some bells and whistles are cool. I think thoughtful animation can improve user experience. You can spot subtle examples on my website. Some designers or developers may not agree with me. Efficiency may be a top priority for them. But, sometimes, it's not even about preferences.

On December 16, 1997, an episode of Pokemon shown on Tokyo TV resulted in migraines, visual distortions, nausea, motion sickness, and seizures in over 500 children. But you don't need a flashy tv series or video game to cause these reactions. Flashing light, patterns of lines, gratings, checkerboards, or other configurations can provoke a photic-induced seizure.[1]

There is a classification of different vestibular disorders. The system is responsible for our spatial orientation. For some people, its work can be disturbed by external triggers. Visually-induced dizziness or vertigo can be caused by a complex, distorted, large field or moving visual stimulus. People can feel an illusion of self-motion without actually moving.[2]

A flashing screen or motion (basically an animation) can trigger considerable physical reactions in some users. Some of them sound like a reaction to a carousel for many people - myself included. So, don't take your users on an involuntarily trip to an amusement park. Don't spin them right 'round baby right 'round.

Reduce motion options

OS manufacturers took care of that people. Every big desktop and mobile OS (macOS, Linux, Windows 7+, iOS, Android) has some accessibility setting for reduced motion. Switching the option disables OS animations.

  • In Windows 10: Settings > Ease of Access > Display > Show animations in Windows
  • In Ubuntu: Terminal > gsettings set org.gnome.desktop.interface enable-animations false
  • in macOS: System Preferences > Accessibility > Display > Reduce motion
  • In iOS: Settings > Accessibility > Motion > Reduce Motion
  • In Android: Settings > Accessibility > Remove animations

This option is also exposed to web users. With the prefers-reduced-motion media query, you can detect if the user prefers animations. It works similarly to the prefers-color-scheme media query. But, it would not work if the browser support is poor. So, let's check it out:

Chart from caniuse.com with prefers-reduced-motion browser support (94.41%)

Browser support is good, so I'll try to make my website more accessible!

Usage

My website is minimalistic, so there are hardly any animations. But there is one good example, the typewriter effect on my home page. I created a typewriter component that takes an array of strings and simulates typing them.

Mdn documentation states: "the prefers-reduced-motion media feature is used to detect if the user has requested that the system minimize the amount of non-essential motion it uses." Even though the mentioned effect neatly conveys what I am often doing - typing - it's mainly for aesthetics. So, it's a good candidate for reduced motion.

CSS media query

This CSS snippet in a styled component is responsible for cursor blinking. Every second, pipe character opacity oscillates between 0 and 1.

CSS
1@keyframes blink {
2 50% {
3 opacity: 0;
4 }
5}
6
7.cursor:after {
8 content: '|';
9 animation: blink 1s step-start infinite;
10 color: var(--color-primary-base);
11}

We can disable this animation using our media query.

CSS
1@media (prefers-reduced-motion: reduce) {
2 .cursor:after {
3 content: '';
4 animation-name: none;
5 }
6}

If a user prefers reduced motion, there won't be animation or the pipe character.

React

But there is another part of the animation - typing. It's implemented in JavaScript. So, I need to get this preference in this language. There is probably a hacky way to implement something similar in pure CSS. I even tried some solutions, but I wasn't satisfied with them. Even though CSS evolves, it has its limitations. And some other types of animations just can't be done in CSS.

In another part of the website, I use window.matchMedia() to detect if the user has a preferred OS theme color. With this method, we can access media query values in JS. I'll use it to check reduced motion preference.

JSX
1const hasReducedMotionPreference = window.matchMedia(
2 '(prefers-reduced-motion: reduce)'
3).matches // Bool value which depends on system settings

Now, we can use this information inside the custom hook.

JSX
1import { useState, useEffect } from 'react'
2
3export const usePrefersReducedMotion = () => {
4 const [hasReducedMotionPreference, setHasReducedMotionPreference] =
5 useState(false)
6
7 useEffect(() => {
8 const mediaQueryList = window.matchMedia('(prefers-reduced-motion: reduce)')
9 const initialPreference = mediaQueryList.matches
10 setHasReducedMotionPreference(initialPreference)
11
12 const listener = (event) => {
13 setHasReducedMotionPreference(event.matches)
14 }
15 mediaQueryList.addEventListener('change', listener)
16 return () => {
17 mediaQueryList.removeEventListener('change', listener)
18 }
19 }, [])
20
21 return hasReducedMotionPreference
22}

The default value of that hook is false - we don't know user preferences yet. You can also set it to true - turn off animations by default. It is more accessible this way, but it caused layout shifts in my case, so I leave it false for now. Then, the useEffect() hook checks OS preference. Depending on the OS setting, the variable hasReducedMotionPreference is updated accordingly. The variable is returned from the hook. The event listener keeps the variable up to date with OS settings. You can see the listener in action. Switch your OS motion setting while watching my home page.

We can reuse the hook in every component that uses animation. I will use it in my landing component, where the typewriter effect is located.

JSX
1import { usePrefersReducedMotion } from '../hooks/usePrefersReducedMotion'
2// Other imports
3
4const Landing = () => {
5 const { t } = useTranslation('components/landing')
6 const hasReducedMotionPreference = usePrefersReducedMotion()
7
8 return (
9 <Hero>
10 <Tile as="header">
11 <H1 aria-label={t('aria')}>
12 {hasReducedMotionPreference ? (
13 t('typewriter.create')
14 ) : (
15 <Typewriter
16 strings={[
17 t('typewriter.design'),
18 t('typewriter.code'),
19 t('typewriter.write'),
20 t('typewriter.create')
21 ]}
22 ></Typewriter>
23 )}
24 </H1>
25 </Tile>
26 </Hero>
27 )
28}
29
30export default Landing

The usage is straightforward. I import the custom hook and use it to return reduced motion preference. Then I use the ternary operator. If the reduced motion option is on, a static string is rendered. Otherwise, the typewriter effect is played.

Diable all animations

"Ok, but it looks like a lot of work. Can't we just disable all animations with one snippet, something like CSS reset?" Actually, we can. Browsing the web, I found a snippet like this:

CSS
1* {
2 /*CSS transitions*/
3 -o-transition-property: none !important;
4 -moz-transition-property: none !important;
5 -ms-transition-property: none !important;
6 -webkit-transition-property: none !important;
7 transition-property: none !important;
8 /*CSS transforms*/
9 -o-transform: none !important;
10 -moz-transform: none !important;
11 -ms-transform: none !important;
12 -webkit-transform: none !important;
13 transform: none !important;
14 /*CSS animations*/
15 -webkit-animation: none !important;
16 -moz-animation: none !important;
17 -o-animation: none !important;
18 -ms-animation: none !important;
19 animation: none !important;
20}

But I don't think it's a good idea. First, I encourage you to minimize the usage of the !important syntax. Using it too often can mess with CSS specificity. Second, not all animations are in CSS. This snippet can't take care of canvas, SVG, or other JS animations. And also, reduced animation doesn't mean no animations. Some animations can improve the user experience. Sometimes the line between essential and non-essential animations is blurry, but even considering the problem is a good start. At the end of writing, I found this article - there are examples of troubling animations, so it may help.

Meticulous preparation of animations and their reduced versions can be tiring. But - as always - it's worth it. Making your website more accessible can save trouble for some people, so more people can enjoy your content.

A newsletter that sparks curiosity💡

Subscribe to my newsletter and get a monthly dose of:

  • Front-end, web development, and design news, examples, inspiration
  • Science theories and skepticism
  • My favorite resources, ideas, tools, and other interesting links
I am not a Nigerian prince to offer you opportunities. I do not send spam. Unsubscribe anytime.

Stay curious. Read more

List of CSS variables in Visual Studio Code.September 14, 20228 min read

Converting design tokens to CSS variables with Node.js

Converting design tokens is an error-prone process - I found about it the hard way. So, I made a simple Node.js script that will help me with that task.

Read post
Five metal gears on a black brackgroundSeptember 23, 202211 min read

Gatsby with Netlify CMS

In this post, we will look closely at a Netlify CMS. It is an example of a new type of CMS that is git-based. We will integrate it with a Gatsby example project.

Read post
Question mark composed with dots on yellow backgroundJuly 23, 20227 min read

Is it native JavaScript? Isn't it??

My first encounter with optional chaining and nullish coalescing operator.

Read post