Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 15.6Advanced ~22 min · shot 6 of 7

MotionConfig, Reduced Motion, Vanilla animate and React Spring

In one line

Motion ignores the reduced-motion setting unless you wrap it in MotionConfig reducedMotion="user"; then movement turns instant while fades and colours still animate. LazyMotion trims the bundle, animate() works without React, and React Spring is the physics-first alternative.

0/7 · 0%

Think of it like this

A building with a master volume switch. Each room has its own speaker, but the switch at the entrance can turn every speaker down at once. MotionConfig is that switch for every motion element below it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

MotionConfig
A wrapper that sets defaults (including the reduced-motion policy) for every Motion component inside it.
reducedMotion
"user", "always" or "never": whether Motion follows the device's reduce-motion setting. The default is "never".
LazyMotion
A wrapper that loads Motion's features separately, used with the lighter m components.
domAnimation / domMax
The feature bundles for LazyMotion: animations and gestures, and the same plus drag and layout.
animate()
Motion's function for animating elements or values outside React.
tension / friction
React Spring's names for a spring's stiffness and damping.

Step by step

01Run it as a normal user

Press Go with the mode on user on an ordinary device. The box slides and turns with a spring, and fades over 300 ms.

02Switch on Reduced motion

Press the timeline's Reduced motion switch. The page reloads as a reduced-motion user. Press Go: the box is at its final place at once, while the opacity track still runs. The console shows Motion's warning.

03Compare the three settings

Choose always on a normal device: movement is instant. Choose never with Reduced motion on: everything animates, ignoring the device. The select changes only the config, so the page itself stays the same.

04Check the cost

Run the vanilla example: animate() makes one transform track per list item. Run the React Spring one: the Timeline stays empty while the box moves.

Try it yourself

  1. 1

    Read the three modes

    Run the first example with Reduced motion off and set the select to always, then never; then switch Reduced motion on and try user, always, never. Measured: user and always leave only an opacity track (the transform is already matrix(0, 1, -1, 0, 180, 0) at 50 ms); never runs a 500 ms transform track and a 450 ms rotation track even on a reduced device.

  2. 2

    Make the box skip the config

    Remove <MotionConfig> and run as a reduced-motion user. Measured at 170 ms in Topic 15.1's example: the box is at 179.2 px, the same as for a user with full motion. Nothing in Motion follows the device until you add the wrapper.

Code & diagrams

MotionConfig: user, always, never

Go moves, turns and fades the box. Switch the timeline's Reduced motion on and try each setting in the select: only `user` and `always` make the movement instant.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.
Vanilla animate(), with a missing target

No React. Three list items enter with a stagger; the button wobbles them. `animate(".nothing", ...)` quietly does nothing.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

animations right after the call: 3
missing target: 0
finished: li 3 opacity 1
React Spring: a spring with no duration

The box moves on `tension` and `friction`. The page presses Go itself and counts the browser animations on the Timeline: there are none.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

moving, animations on the timeline: 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Trust the CSS media query for Motion

Add @media (prefers-reduced-motion: reduce) { .box { transition: none; animation: none; } } to the example and remove MotionConfig.

terminal
$ Switch on Reduced motion and press Go
── what you'll see ──
The box still moves with a spring: the track `transform` is 500 ms long (measured at 50 ms: the box is at 70.2 px and already turning, with a 500 ms transform track and a 450 ms rotation track).

Myth vs fact

Myth

Motion respects prefers-reduced-motion automatically.

Fact

Not unless you opt in: the default of MotionConfig is reducedMotion: "never". Without a wrapper, Motion ignores the device.

Myth

LazyMotion always shrinks the bundle.

Fact

domAnimation saves about 14 KB gzip here. Once you add layout or drag with domMax, you carry nearly the full size (105.4 KB vs 105.3 KB).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    MotionConfig also sets a default transition for everything below it: <MotionConfig transition={{ duration: 0.2 }}> gives a whole feature the same timing, which suits motion tokens (Phase 18).

  • ▸

    Reduced motion is a setting you can lose track of: check that the transform is final before any opacity ends, as the check in the exercises does. A reduced user still needs a visible change, which the opacity tween gives.

  • ▸

    Bundle numbers depend on the bundler and tree-shaking. The motion/mini figure (3.4 KB) applies only to animate() and WAAPI features: no springs, no stagger, no React components.

Remember this

  1. 1

    The problem this solves: the CSS rules of Phase 7 cannot reach animations that JavaScript runs. Motion's transform animations are WAAPI or JavaScript, so @media (prefers-reduced-motion) does not stop them. Before this topic, a Motion page ignores the person who asked for less motion. Measured: the first example of Topic 15.1, run as a reduced-motion user, still springs to 179 px.

  2. 2

    **The default is never.** MotionConfig's reducedMotion can be "user" (follow the device setting), "always" or "never", and the default, read from the installed source, is "never". Wrap your app once: <MotionConfig reducedMotion="user">. The configurator does exactly this, with the site's own setting from Phase 7 on top.

  3. 3

    What it changes. With the setting active, values that move something (every transform: x, y, scale, rotate, skew; and width, height, top, left, right, bottom) jump to their target at once, and layout animations are instant. Everything else keeps animating. Measured on the example below, as a reduced-motion user: a box animating x, rotate and opacity had its transform at the final matrix(0, 1, -1, 0, 180, 0) after 50 ms with no transform track, while opacity still ran a 300 ms tween. This is Phase 7's rule, reduce rather than remove, built in. "always" on a normal device behaves the same; "never" on a reduced device animates everything.

  4. 4

    What it does not change. Drag keeps working, and the release spring still runs (Topic 15.5). Animations you start yourself with animate() are yours to guard. In development Motion logs You have Reduced Motion enabled on your device. Animations may not appear as expected.. For more information and steps for solving, visit https://motion.dev/troubleshooting/reduced-motion-disabled when the device is reduced and Motion reads it (even with never, or when only useReducedMotion() reads it), and when you set always (measured; a normal device with user or never prints nothing), so a developer who tests with reduced motion sees why things jump.

  5. 5

    LazyMotion. The full motion component carries every feature. LazyMotion with m components loads features separately: domAnimation (animations, variants, exit, hover, tap, focus) or domMax (adds drag and layout). Measured by bundling a tiny entry file for each case with esbuild (--bundle --minify, production React) against the installed packages, then gzipping the output: React alone 60.2 KB; React plus motion.div 105.3 KB (45.1 KB more); React plus LazyMotion, domAnimation and m.div 91.1 KB (30.9 KB more); with domMax 105.4 KB (45.2 KB more). So domAnimation saves about 14 KB, and a page that uses layout or drag saves nothing. Totals move by a few KB with the bundler, its version and its flags, so trust the differences, not the totals. features can also be an async function that imports the bundle later, and strict makes a stray motion component throw.

  6. 6

    **Vanilla animate().** Outside React, animate("li", { x: [0, 20, 0] }, { duration: 0.3 }) takes a selector or element, returns controls (finished, pause, cancel, time), and uses the same hybrid engine: three li made three transform tracks of 300 ms (measured). stagger(0.1) spreads delays. A selector that matches nothing is not an error: it returns controls with a duration of 0 (measured). Bundled alone it is 24.0 KB gzip; motion/mini (WAAPI only) is 3.4 KB (same esbuild measurement).

  7. 7

    React Spring, in one section. React Spring models everything as springs (tension, friction, mass): there is no duration. useSpring returns animated values that animated.div writes to the element's style from JavaScript on each frame, with no React re-render. It never uses WAAPI (measured: 0 animations on the Timeline while the box moved), so every frame is main-thread work. It brings useTransition for mounting and leaving (like AnimatePresence), useTrail and useChain, and adds about 17.0 KB gzip over React for useSpring and animated (measured). It has no built-in layout or shared-element animation. Choose it when physics and gestures lead the design; choose Motion when you want layout animation, exits, variants and WAAPI.

Explain it without notes

01

Why does a Motion page ignore reduced motion by default, and how do you fix it?

02

When would you choose LazyMotion, and when would it not help?

Practice

01

Make the app follow the device's reduced-motion setting, and also a site toggle that can force "reduced" or "full".

02

Switch an app's motion.div components to m.div and load the animation features after the first paint.

Trade-offs

  • ↔

    Motion gives layout animation, exits and WAAPI at about 31 to 45 KB gzip over React. React Spring is smaller (about 17.0 KB over React) and physics-pure, but has no layout animation and no browser animations.

  • ↔

    Instant movement under reduced motion changes what a person sees: design the end state to be clear without the journey (Phase 7).

Done when you can

  • You can wrap an app so Motion follows the device's reduced-motion setting

  • You can say what reducedMotion changes and what it leaves alone

  • You can choose between motion and LazyMotion with domAnimation or domMax, and quote the size difference

  • You can use vanilla animate() and say where React Spring fits