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, 2022I 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:
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}67.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'23export const usePrefersReducedMotion = () => {4 const [hasReducedMotionPreference, setHasReducedMotionPreference] =5 useState(false)67 useEffect(() => {8 const mediaQueryList = window.matchMedia('(prefers-reduced-motion: reduce)')9 const initialPreference = mediaQueryList.matches10 setHasReducedMotionPreference(initialPreference)1112 const listener = (event) => {13 setHasReducedMotionPreference(event.matches)14 }15 mediaQueryList.addEventListener('change', listener)16 return () => {17 mediaQueryList.removeEventListener('change', listener)18 }19 }, [])2021 return hasReducedMotionPreference22}
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 imports34const Landing = () => {5 const { t } = useTranslation('components/landing')6 const hasReducedMotionPreference = usePrefersReducedMotion()78 return (9 <Hero>10 <Tile as="header">11 <H1 aria-label={t('aria')}>12 {hasReducedMotionPreference ? (13 t('typewriter.create')14 ) : (15 <Typewriter16 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}2930export 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.