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.
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.
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
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 readsscrolling story: 5 chart(s), 5 steps of text. This is what a browser withoutIntersectionObserver, or a visitor who chose the list, gets. - 2
Drop one number from the text
In the third example, change the step text
Friday 18 July: 4,380 rides.toFriday: 4,380 rides.. The first audit line now readsreal text numbers on the chart that the text does not say: 18, because the chart's label says18 Jul. The check compares what the chart writes with what the text says.
Code & diagrams
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
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: trueThe 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
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 pxA 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
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: 0Break 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;.
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-motioncan change while the page is open. Listen withquery.addEventListener("change", handler)and readquery.matchesagain, rather than caching it at load. Reduce more than transitions: parallax, auto-playing scroll animations andscroll-behavior: smoothare motion too. WCAG 2.2 has a criterion for it (2.3.3, Animation from Interactions, level AAA), and2.2.2covers anything that moves by itself for over five seconds. - ▸
Do not announce every step with
aria-liveas 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:
100vhis the largest the viewport gets (with the address bar hidden), so content sized with it can be cut off at first.svhis the smallest,lvhthe largest,dvhfollows the bar as it moves. A sticky chart height insvhstays put while advhheight would resize the chart during scrolling and re-run your observer. - ▸
Print is a fallback too: a
@media printrule 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
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
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)").matchesis 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
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
Phones first. At 320 px wide the chart's
widthmust follow its box: give the SVG aviewBoxandwidth: 100%; height: auto. A sticky chart takes the top of the screen, so give it about 40 % of the height (40svh: thesvhunit 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
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 whenIntersectionObserveris 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
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 adetailselement; start with a skip link; and add noaria-liveregion 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
How does a page honour prefers-reduced-motion, and what must stay the same?
How do you make the chart fit a 320 px screen, and why is a sticky chart a problem there?
What is a stacked fallback, when is it used, and what does it need?
What does a screen reader user need from a scrolly story, and what should you avoid?
Practice
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.
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.