Command Palette

Search for a command to run...

Hectal
PHASE 17Advanced ~43 min· topic 5 of 6

Topic 17.5

Stories on Phones and for Everyone

In one line

Most readers of a public story arrive on a phone, and some cannot or will not watch it move. Reduced motion, a layout that fits 320 px, a version that needs no scrolling trick, and text that carries every number for a screen reader are part of the story, not extras.

Think of it like this

A public notice board outside a town hall. It must be readable by a person in a wheelchair, a person who is blind (someone reads it out), a child, and a person in a hurry in the rain. If it only works for a tall person with good eyes who stops and waits, it has failed some of the people it was put there for.

Words you'll meet

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

prefers-reduced-motion
A media query that is true when the visitor asked their system for less animation.
viewBox
The SVG's own coordinate window. With width: 100% it lets the chart scale to any screen (Topic 1.4).
svh
A CSS unit: 1 % of the viewport height with the phone's browser bars showing, so layouts do not jump when the bars hide.
progressive enhancement
Starting from a version that works with nothing special, and adding the scrolling effect on top where the browser supports it.
fallback
The simpler version used when the full one cannot run: here, the stacked list of steps.
aria-hidden
An attribute that hides an element from screen readers. Use it only when the same facts are available in the text.
skip link
A first link on the page that jumps past a long section, for keyboard and screen reader users.

Step by step

01Ask for less motion

The first example runs the same change, seasons to e-bikes, twice. With reduced true the duration is 0 and the picture is final before the next line of code; with it false, the duration is 500 ms and the picture is only final when the promise from show resolves.

The rule is one line: const duration = reduced ? 0 : 500.

the whole rulewhole filejs
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
chart.show(name, reduce ? 0 : 500);

02Fit a phone

In the second example the chart has a viewBox and width: 100%, so in a 290 px frame it is 290 px wide and 203 px tall. It is position: sticky at the top with a white background, so the step text slides underneath instead of through it.

03Build a stacked version

The same STEPS list draws a different page: one article per step, each with its own chart drawn straight in its state. It has five charts instead of one sticky one, and needs no observer. The log of the second example counts what each version has.

Build a stacked versiondiagram
Rendering diagram…

04Put every fact in the text

The third example builds the page a screen reader meets: a skip link, a title, five headed sections, the chart (hidden from the reader), and a data table. It checks that each number printed on the chart is in the step's text.

The lazy text fails the check for seven numbers: 18, 4,380, 14, 439, 28 and 38. Nothing in the picture is wrong, and nothing on the page is spoken.

Try it yourself

  1. 1

    Force the stacked version

    In the second example, change const flags = { scrolly: true } to { scrolly: false }. The left panel now builds the stacked list too: the first log line reads scrolling story: 5 chart(s), 5 steps of text. This is what a browser without IntersectionObserver, or a visitor who chose the list, gets.

  2. 2

    Drop one number from the text

    In the third example, change the step text Friday 18 July: 4,380 rides. to Friday: 4,380 rides.. The first audit line now reads real text numbers on the chart that the text does not say: 18, because the chart's label says 18 Jul. The check compares what the chart writes with what the text says.

Code & diagrams

Reduced motion: same picture, no travel New tab

The media query's text is real. The visitor's setting is not available in the preview, so the example uses a flag for it: on a real page, `reduced` is the result of `matchMedia(...).matches`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

the question the page asks the browser: (prefers-reduced-motion: reduce)
reduced motion on  | duration 0 ms | right after the call: true | when it finished: true
reduced motion off | duration 500 ms | right after the call: false | when it finished: true
A phone layout and a stacked fallback from one list of steps New tab

The code scrolls nothing here: it counts what each version builds. The first panel's chart is hidden from screen readers because the steps carry the facts (next example); the stacked version's charts keep their labels.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

scrolling story: 1 chart(s), 5 steps of text, needs IntersectionObserver: true
stacked fallback: 5 chart(s), 5 steps of text, works without IntersectionObserver: true
chart width inside the 290 px phone frame: 290 px, height: 203 px
What a screen reader meets: reading order and missing numbers New tab

A failure that prints no error: the lazy page looks the same on screen and says almost nothing aloud. The audit finds it by comparing the chart's labels with the text.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

real text reading order: a > h1 > 5 x (h2 > p) > figure (hidden from screen readers) > details
real text numbers on the chart that the text does not say: none
real text live regions that talk while scrolling: 0
lazy text reading order: a > h1 > 5 x (h2 > p) > figure (hidden from screen readers) > details
lazy text numbers on the chart that the text does not say: 18, 4,380, 14, 439, 28, 38
lazy text live regions that talk while scrolling: 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

Animate even when the visitor asked for less

In the first example, replace const duration = reduced ? 0 : 500; with const duration = 500;.

terminal
$ Console
── what you'll see ──
reduced motion on | duration 500 ms | right after the call: false | when it finished: true
reduced motion off | duration 500 ms | right after the call: false | when it finished: true

Myth vs fact

Myth

Reduced motion means the story must not change.

Fact

It means the changes arrive without travel. The chart still goes to the next state, at once. Information is not removed, only movement.

Myth

A screen reader can read a chart if it has an aria-label.

Fact

One label for a chart that changes five times says almost nothing. Put the facts in the text, in order, and give the chart a data table. Hiding the decorative chart is only safe when the text carries everything.

Myth

Phones just need a smaller version of the desktop page.

Fact

A sticky chart that fills the screen hides the text. The chart must take a share of the height, scale with a viewBox, and the text must be readable without hover.

Pro corner

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

  • ▸

    prefers-reduced-motion can change while the page is open. Listen with query.addEventListener("change", handler) and read query.matches again, rather than caching it at load. Reduce more than transitions: parallax, auto-playing scroll animations and scroll-behavior: smooth are motion too. WCAG 2.2 has a criterion for it (2.3.3, Animation from Interactions, level AAA), and 2.2.2 covers anything that moves by itself for over five seconds.

  • ▸

    Do not announce every step with aria-live as the reader scrolls: a screen reader that already reads each paragraph would then read it twice, and fast scrolling would queue speech for steps already gone. The step text in DOM order is the announcement. Keyboard users move with Space and Page Down, which scrolls the page the same way, so the observer works for them unchanged.

  • ▸

    Viewport units are not all alike on phones: 100vh is the largest the viewport gets (with the address bar hidden), so content sized with it can be cut off at first. svh is the smallest, lvh the largest, dvh follows the bar as it moves. A sticky chart height in svh stays put while a dvh height would resize the chart during scrolling and re-run your observer.

  • ▸

    Print is a fallback too: a @media print rule that shows the stacked layout and hides the sticky one gives a printable page, and search engines and link previews read the same text, so the facts in the text also help the page be found.

Remember this

  1. 1

    The problem this solves: the city's website is open to everyone. A sticky chart that morphs while you scroll is lovely on a laptop. On a phone it can fill the whole screen, on a screen reader it can be silent, and for a person who gets dizzy from motion it can make them ill. Lina's page must work for all of them, with the same facts.

  2. 2

    Reduced motion is a setting the visitor already made. Operating systems have a switch for it, and browsers pass it on as the media query (prefers-reduced-motion: reduce). In code, window.matchMedia("(prefers-reduced-motion: reduce)").matches is true when the visitor asked for less motion. Honour it by making every transition last 0 ms and by turning smooth scrolling off. The picture does not change: it arrives at once. The first example prints the rule and its effect. The page cannot read the visitor's setting in the preview, so it uses a flag.

  3. 3

    Less motion is not no story. With reduced motion on, the line still goes from the seasons to the e-bike share, and the labels still appear. The example checks it: right after the call the line already equals the final picture (true); with motion on it is not yet (false), and only becomes equal when the transition ends. The information is the same, and only the travel is removed.

  4. 4

    Phones first. At 320 px wide the chart's width must follow its box: give the SVG a viewBox and width: 100%; height: auto. A sticky chart takes the top of the screen, so give it about 40 % of the height (40svh: the svh unit is the viewport with the browser's address bar showing, so it does not jump), and let the step text scroll underneath with an opaque background. Text should be at least 16 px. On a phone there is no hover, so no step may depend on a tooltip.

  5. 5

    Give the story a non-scroll version. A scrolling story needs IntersectionObserver, JavaScript and a visitor willing to scroll slowly. A stacked version needs none of them: each step is an article with its own text and its own small chart, one under another. The second example builds both from one list of steps. It uses the stacked version when IntersectionObserver is missing or a flag says so, and a real page offers a "Show as a list" control and uses the stacked layout for print.

  6. 6

    Screen readers hear the DOM, not the picture. A sticky chart that changes as you scroll is a moving target for a screen reader. The pattern that works: put every fact in the step's own text, in DOM order, with real headings; hide the decorative chart with aria-hidden="true"; offer a data table in a details element; start with a skip link; and add no aria-live region that talks while the visitor scrolls. The third example audits it. It lists the reading order and checks that every number the chart labels is also said in the step's text. With real text there are none missing. With the lazy text "Look at the dot on the chart", seven numbers are missing: a chart that is fine to look at and empty to listen to.

Explain it without notes

01

How does a page honour prefers-reduced-motion, and what must stay the same?

02

How do you make the chart fit a 320 px screen, and why is a sticky chart a problem there?

03

What is a stacked fallback, when is it used, and what does it need?

04

What does a screen reader user need from a scrolly story, and what should you avoid?

Practice

01

Write durationFor(reduced, ms) that returns 0 when reduced is true and ms otherwise, and returns 0 for a negative or non-number ms too. Test five inputs.

02

Write missingNumbers(labels, text) that returns the numbers in the labels that the text does not contain, like the third example. Test it with a label list that is empty and with a label that has no digits.

Trade-offs

  • ↔

    Hiding the chart from screen readers keeps the page quiet and puts the whole job on the text. That is right when the text carries every number, and wrong if the chart holds data the text does not.

  • ↔

    A separate stacked version is more code and more charts on the page, and it is the version that works everywhere, including print. Many teams ship it as the default and add the scrolling layer on top.

Done when you can

  • I can make transitions last 0 ms when the visitor asked for reduced motion.

  • I can make the chart scale to 320 px and give it a safe sticky height.

  • I can build a stacked version from the same list of steps.

  • I can put every number on the chart in the step's text and check it.

  • I can name what not to do for screen readers: one changing label and talking live regions.