Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 7.5Intermediate ~23 min · shot 5 of 7

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.

0/7 · 0%

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 as element.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.

Write it on the pagediagram
Rendering diagram…

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. 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) or fade. *Full* is the only choice that moves the bike on a reduced device.

  2. 2

    Read the line under the bike

    Choose Reduced: the line says data-motion: reduced allowed: false. Choose Follow my device: the attribute reads undefined (removed) and allowed is the device's answer.

Code & diagrams

The choice, the attribute and the six combinations

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`.

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

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 device
Storage blocked

A 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.

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

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: SecurityError

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

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.

terminal
$ Run the example
── what you'll see ──
Uncaught SecurityError: Access is denied for this document.
The toggle never gets set up, so no console line is printed.

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. 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. 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 localStorage and written as data-motion="reduced" or "full" on <html>; for *follow* the attribute is removed.

  3. 3

    Make CSS read the attribute. A media query cannot see the attribute, so motion-safe.css repeats 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, ride becomes fade for *reduced*, and *full* restores ride on a reduced device.

  4. 4

    Apply it before first paint. main.js is 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 reads localStorage and sets data-motion immediately (the build topic shows it).

  5. 5

    When storage is blocked. In private modes and with strict settings, localStorage can throw. Every access sits in try/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

01

Why does a site setting need to override the device setting in both directions?

02

Why is the choice applied from an inline script in the head?

Practice

01

What is stored for each of the three choices?

02

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/catch

  • You handle the device setting and other tabs changing