Shots in this scene
A Motion Toggle on the Site
In one line
A site setting with three choices (follow my device, reduced, full) stored in localStorage and applied as data-motion on <html> before first paint. It overrides the device setting in both directions and keeps working when storage is blocked.
Think of it like this
A TV remote has a "mute" button even though the room may already be quiet. The visitor's own choice on this site is the button on the remote. The device setting is the default volume in the building.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- localStorage
- A small key-value store in the browser that survives closing the page. Reading or writing it can throw if the browser blocks storage.
- data attribute
- An HTML attribute starting with
data-, readable in CSS as[data-motion="reduced"]and in JavaScript aselement.dataset.motion. - override
- A choice that wins over another: here, the site choice wins over the device setting.
- first paint
- The first time the browser draws the page. Anything that changes the page after it can be seen as a flash.
Step by step
01Store the choice
Save reduced or full under one key. For *follow my device* remove the key, so there is nothing to get out of date.
02Write it on the page
Set document.documentElement.dataset.motion, or delete it for *follow*. CSS and scripts both read this one place.
03Make the CSS agree
Repeat the calm swaps under :root[data-motion="reduced"]. Put the movement back for full inside the reduce media query. Leave *follow* to the media query alone.
04Survive the edge cases
Wrap every storage access in try/catch. Notify listeners on every change. Test with storage blocked, with the device setting on and off, and with two tabs open.
Try it yourself
- 1
Read the six combinations
In the first example press Ride with each radio, once normally and once with the timeline's Reduced motion on. The track name is
ride(movement) orfade. *Full* is the only choice that moves the bike on a reduced device. - 2
Read the line under the bike
Choose Reduced: the line says
data-motion: reduced allowed: false. Choose Follow my device: the attribute readsundefined(removed) andallowedis the device's answer.
Code & diagrams
This phase's module is inlined. Choose each radio and press Ride. Device normal: `ride`, `fade`, `ride` for follow, reduced, full. With the timeline's Reduced motion on: `fade`, `fade`, `ride`.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
reduced: data-motion reduced, saved reduced, allowed false
full: data-motion full, saved full, allowed true
system: data-motion removed, saved null, allowed depends on the deviceA simulated browser that blocks storage: `localStorage` throws a `SecurityError` on every access. The choice still applies for this visit; it is only not remembered. `undefined` means the attribute was removed.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
page loaded: choice is system (reading storage failed, so it follows the device)
chose reduced: data-motion reduced, allowed false, saved: not possible: SecurityError
chose system: data-motion undefined, saved: not possible: SecurityErrorBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call storage without try/catch
In "Storage blocked", in readSaved, delete the try { and } catch { return "system"; } lines so only the localStorage.getItem line is left, and run.
Myth vs fact
Myth
A site toggle makes the media query unnecessary.
Fact
Most visitors never open your footer. The media query is what protects them. The toggle is a second, optional control on top.
Remember this
- 1
The problem this solves: the device setting is one switch for every site. A visitor may want calm on yours only, or may have the device setting on for the system's animations and still want your site's movement. A person on a shared computer cannot change the operating system. A site switch gives a second, closer control (the A List Apart article recommends a site-wide toggle).
- 2
Three choices, one rule. *Follow my device*: the media query decides. *Reduced*: calm, whatever the device says. *Full*: normal motion, whatever the device says. The choice is saved in
localStorageand written asdata-motion="reduced"or"full"on<html>; for *follow* the attribute is removed. - 3
Make CSS read the attribute. A media query cannot see the attribute, so
motion-safe.cssrepeats the calm rules under:root[data-motion="reduced"], and for *full* puts the movement back inside@media (prefers-reduced-motion: reduce)under:root[data-motion="full"]. In the example,ridebecomesfadefor *reduced*, and *full* restoresrideon a reduced device. - 4
Apply it before first paint.
main.jsis a module and runs after parsing, so the first frame could show motion before the attribute is set. The fix is a tiny ordinary<script>in<head>, before the stylesheets: it readslocalStorageand setsdata-motionimmediately (the build topic shows it). - 5
When storage is blocked. In private modes and with strict settings,
localStoragecan throw. Every access sits intry/catch. The choice still applies for the visit (a variable and the attribute); it is only not remembered. The second example simulates blocked storage.
Explain it without notes
Why does a site setting need to override the device setting in both directions?
Why is the choice applied from an inline script in the head?
Practice
What is stored for each of the three choices?
Write the head script that applies a saved reduced or full choice, ignoring any other value.
Done when you can
You can describe the three choices and how each combines with the device setting
You apply the choice before first paint
You wrap every storage access in
try/catchYou handle the device setting and other tabs changing