Accessibility
Animation helps people follow what changes on a page, but movement can also make people dizzy or hide information from them. This page covers reduced motion and a few habits that keep animated interfaces usable for everyone.
Reduced motion
Operating systems have a setting to reduce motion, exposed to the web as the
prefers-reduced-motion media query. People with vestibular disorders turn it on because large
movements on screen can cause dizziness or nausea.
MotionConfig reducedMotion
MotionConfig decides how the motion components inside it respond to the
setting:
import { MotionConfig } from "@motion-s/solid"
function Root() {
return (
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
)
}| Value | Behavior |
|---|---|
"never" | Default. Animations run whatever the device setting. |
"user" | Reduce motion when the device setting is on. |
"always" | Always reduce motion. |
When motion is reduced:
- values that move or resize an element change at once instead of animating: transforms (
x,y,scale,rotate, …) andwidth,height,top,left,right,bottom, - layout animations change at once,
- other values still animate:
opacity, colors,filter,clipPath, and so on.
So a modal that slides and fades in still fades, without the slide. A motion component reads the
setting when it mounts.
A transition can override it for the values it animates, with reduceMotion: true to change
them at once, false to always animate them.
// A progress bar's movement carries information: keep it.
<motion.div
animate={{ scaleX: progress() }}
transition={{ reduceMotion: false }}
/>useReducedMotion
useReducedMotion returns an accessor of the device setting. Use it
for what MotionConfig doesn't cover: replacing a slide with a fade of your choice, pausing an
autoplaying video, turning off parallax effects driven by scroll.
const shouldReduceMotion = useReducedMotion()
<motion.div
initial={shouldReduceMotion() ? { opacity: 0 } : { opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
/>Gestures and the keyboard
whileTapandonTapwork with the keyboard: pressing Enter on a focused element starts a tap, releasing it ends the tap. An element withwhileTapthat isn't focusable by default (like adiv) getstabindex="0", unless it already has atabindex.whileFocusis active while the element matches:focus-visible, so it shows for keyboard focus but usually not for a mouse click. Use it for focus styles that animate.whileHoverignores touch: it only responds to a mouse or a pen. Don't hide controls or information behind a hover: touch and keyboard users can't reach them.- Prefer a
buttonto adivwith a tap gesture: it already handles focus, Space and screen readers.
Good habits
-
Don't convey information only through motion. A shake on a wrong password also needs an error message that a screen reader can announce.
-
Avoid large movements across the screen, zooms, spins and parallax for decoration, especially over large areas. These are the most common vestibular triggers.
-
Keep interface transitions short. Long ones delay people who are waiting to act.
-
Let people pause anything that moves for more than five seconds, like a carousel or a looping animation.
-
Don't flash content more than three times per second.
-
When a dialog or a panel animates in, move focus to it as usual. An element that's exiting is still in the DOM until its exit ends, and can still be focused and clicked. To prevent that, make its content inert with
useIsPresent:function Panel() { const isPresent = useIsPresent() return ( <motion.div exit={{ opacity: 0 }}> <div inert={!isPresent()}>…</div> </motion.div> ) }The props of an exiting
motioncomponent don't update, so putinerton an element inside it.