A page that changes after it loads carries a hidden contract: whatever a sighted user
perceives in the change, everyone else must be able to perceive and control too. Four
defects break that contract again and again. A result count or a “Saved” message
updates in the DOM but is never announced, so a screen reader user has no idea it
happened. A parallax or autoplaying animation plays full-force with no way to calm it,
upsetting people with vestibular disorders. Something flashes fast enough to risk a
seizure. Or a timeout or auto-advancing carousel takes the decision away from anyone
who needs more time.
This lesson works through all four. None requires a framework — a live region is one
attribute, a reduced-motion path is one media query, the flash limit is a design
constraint, and time limits just need a control. The wins come from respecting the
user’s context, not from adding complexity.
What you’ll learn
How to announce silent DOM updates with a live region
(role="status", aria-live="polite", or
role="alert" for errors); how to give motion a
prefers-reduced-motion path and a pause control; why flashing must stay
under three flashes per second; and how to make time limits adjustable, with the
ability to turn them off, extend, or pause.
Standards this lesson maps to
Standard
Criterion
Level
What it requires
WCAG 2.2
4.1.3 Status Messages
AA
Status changes are exposed to assistive technology through a role or property, without moving focus.
WCAG 2.2
2.2.1 Timing Adjustable
A
For each time limit, the user can turn it off, adjust it, or extend it (with limited exceptions).
WCAG 2.2
2.2.2 Pause, Stop, Hide
A
Moving, blinking, or auto-updating content lasting more than 5 seconds can be paused, stopped, or hidden.
WCAG 2.2
2.3.1 Three Flashes or Below Threshold
A
Nothing flashes more than three times per second, or the flash stays below the general and red thresholds.
WCAG 2.2
2.3.3 Animation from Interactions
AAA
Motion animation triggered by interaction can be disabled, unless essential to the functionality.
EN 301 549
9.2.2.2 / 9.2.3.1 / 9.4.1.3 (incorporates WCAG)
—
European harmonised standard; references the WCAG A/AA set including timing, flashing, and status criteria.
Section 508
502.3 / 504 (incorporates WCAG A & AA)
—
US federal ICT must meet WCAG 2.0 Level A and AA, including timing and flashing limits.
ADA Title II
WCAG 2.1 AA (DOJ rule)
AA
US state/local government web content must conform to WCAG 2.1 AA.
The four problems we’ll fix
Each card below isolates one common defect in dynamic content or motion. For every
issue you get a plain-language statement of the problem, a Bad example
(shown as escaped, non-running code so nothing on this page moves, flashes, or breaks),
a Good example, the copyable Code, and an ordered
fix.
Status updates not announced
WCAG 2.2 · 4.1.3AAEN 301 549Section 508
When script changes the page without moving focus —
writing “3 results” after a filter, flashing a “Saved” confirmation, or revealing
an async error — a sighted user sees it at once, but a screen reader user is told
nothing. The accessibility tree updated, yet no event fired to announce it. The fix
is a live region: a container that is already in the DOM on load and is
marked so that assistive technology watches it. When you write text into that
region, it is spoken automatically, with focus left exactly where the user put it.
Use role="status" / aria-live="polite" for routine
messages and role="alert" (implicitly assertive) for errors that must
interrupt.
Bad
The result count is injected into a plain <div> with no live
semantics. Sighted users see “3 results”; screen reader users hear silence.
bad-status.html
<div id="count"></div>
<script>
// Updates the text, but nothing announces the change.
document.getElementById('count').textContent = '3 results';
</script>
Good
The container exists on load with role="status" and
aria-live="polite". Writing text into it is announced after the
user’s current speech finishes, without stealing focus.
good-status.html
<div id="count" role="status" aria-live="polite"></div>
<script>
// The empty live region was already in the DOM on load;
// writing into it is announced politely.
document.getElementById('count').textContent = '3 results';
</script>
Code
For an error that must interrupt, use role="alert" (assertive by
default). Keep the region in the DOM from the start and only change its text —
adding the region and its message at the same time can be missed.
live-region-alert.html
<!-- Polite: routine confirmations -->
<p role="status" aria-live="polite" id="save-msg"></p>
<!-- Assertive: errors that must interrupt -->
<p role="alert" id="form-error"></p>
<script>
saveMsg.textContent = 'Draft saved'; // announced politely
formError.textContent = 'Could not save. Try again.'; // interrupts
</script>
How to fix
Add a live region — role="status" with
aria-live="polite" for routine messages, role="alert"
for errors — and put it in the DOM before the update.
Update only the region’s text content. Don’t create the region and its
message together, and don’t move focus to it.
Keep messages short and self-contained; the user hears them out of context,
so “3 results” beats a bare “3”.
Reserve assertive announcements for genuine interruptions — overusing
alert talks over everything else the user is doing.
Verify with a screen reader that the message is spoken and that focus stays
where the user left it.
Motion with no reduced-motion path
WCAG 2.2 · 2.3.3AAA2.2.2AEN 301 549
Parallax, large sliding transitions, zooming hero
effects, and autoplaying animation play at full strength for everyone — including
people whose vestibular system reacts to large-scale motion with dizziness, nausea,
or disorientation. Browsers expose a user setting, “reduce motion”, through the
prefers-reduced-motion media query; honouring it lets you remove or
shrink animation for the people who asked the operating system to calm it down,
while keeping it for everyone else. Disabling interaction-triggered motion this way
meets 2.3.3; separately, any content that auto-moves for more than five
seconds also needs a pause, stop, or hide control under 2.2.2.
Bad
The animation is unconditional. There is no reduced-motion path and no pause,
so a user who set “reduce motion” still gets the full effect.
bad-motion.css
.hero {
animation: drift 8s linear infinite; /* always on, no opt-out */
}
@keyframes drift {
from { transform: translateX(0) scale(1); }
to { transform: translateX(-40px) scale(1.1); }
}
Good
Motion is the default, but a prefers-reduced-motion: reduce block
removes it for users who asked. The animation is short and decorative, so
stripping it changes nothing functional.
good-motion.css
.hero {
animation: drift 8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.hero {
animation: none; /* no drift, no parallax */
}
}
Code
Because the drift runs longer than five seconds, also give it a real pause
control (2.2.2). The script flips a class; the media query and the pause work
together, and a global reduced-motion reset is a safe baseline.
Wrap non-essential animation in
@media (prefers-reduced-motion: reduce) and remove or shrink the
motion there (set animation: none or swap to a fade).
Keep a small transform/opacity change if you need feedback;
“reduce” means less motion, not necessarily none.
For anything that auto-moves longer than five seconds, add a visible pause,
stop, or hide control (2.2.2) with a clear accessible name and
aria-pressed state.
Avoid large-area parallax and zoom that tracks scroll; these are the
effects most likely to provoke a vestibular reaction.
Test with the OS “reduce motion” setting on, and confirm the pause control
works by keyboard.
Flashing content
WCAG 2.2 · 2.3.1AEN 301 549Section 508
Content that flashes more than three times in any one
second can trigger a seizure in people with photosensitive epilepsy. This is the
most serious harm in the whole guide: it can cause real physical injury. The rule
is firm — nothing may flash more than three times per second unless the flashing
area is small and stays below the general and red flash thresholds. Saturated red
is especially dangerous and has its own, stricter limit. A blinking alert badge, a
rapid strobe in a video, an animated GIF, or a “look here!” attention effect can
all cross the line. The safest engineering answer is simple: don’t build fast
flashing at all, and never use flashing to draw attention.
Bad
This badge toggles full visibility every 150 ms — over six flashes a
second across a saturated red block. It is exactly the pattern 2.3.1 forbids.
bad-flash.css
.alert {
background: #ff0000;
/* ~6.7 flashes per second — over the limit, saturated red */
animation: strobe 0.15s steps(1) infinite;
}
@keyframes strobe {
50% { opacity: 0; } /* full on/off blink */
}
Good
Don’t flash to get attention — change state once and keep it. A static colour
plus an icon and text conveys urgency with zero flashing.
If a subtle pulse is genuinely wanted, keep it slow — at most one cycle per
second, well under the three-flash limit — use a small opacity range rather than
full on/off, and still respect reduced motion.
Never let anything flash more than three times in any one second — design
the limit in, don’t test it out afterwards.
Don’t use flashing to draw attention. Convey urgency with steady colour, an
icon, text, and a live-region announcement instead.
Treat saturated red flashing as off-limits; it carries a stricter threshold
and the highest risk.
Audit imported media too — autoplaying video, GIFs, and ads can flash even
when your own CSS doesn’t.
If you must show motion, run any uncertain content through a flash-analysis
tool (such as PEAT) before release.
Time limits
WCAG 2.2 · 2.2.1A2.2.2AEN 301 549ADA Title II
Two patterns put the user on a clock they didn’t set:
a session that times out and discards their work, and a carousel that auto-advances
before they’ve finished reading a slide. Both fail people who read slowly, navigate
by keyboard or screen reader, or have motor or cognitive disabilities — anyone for
whom “a few seconds” is not enough. Timing Adjustable (2.2.1) requires
that, for each time limit, the user can turn it off, adjust it to at least ten times
the default, or extend it with a simple action after a warning. Pause, Stop,
Hide (2.2.2) requires that auto-advancing content can be paused. The pattern is
the same for both: warn before the limit bites, and hand control to the user.
Bad
The session ends silently after a fixed timeout, and the carousel advances on
its own with no pause. The user gets no warning and no control.
bad-timeout.js
// Logs the user out after 15 min — no warning, no way to extend
setTimeout(logout, 15 * 60 * 1000);
// Carousel jumps to the next slide every 3s, always, no pause
setInterval(nextSlide, 3000);
Good
Before the session ends, a warning appears in a live region with an
Extend button, so the user can stay (2.2.1). The carousel exposes a
pause control and stops auto-advancing on focus or hover.
good-timeout.html
<div role="alertdialog" aria-labelledby="to-title" hidden id="timeout">
<h2 id="to-title">Still there?</h2>
<p>Your session ends in 2 minutes.</p>
<button type="button" id="extend">Stay signed in</button>
</div>
<script>
// Warn 2 min before the limit; let the user extend
setTimeout(showTimeoutWarning, 13 * 60 * 1000);
extend.addEventListener('click', resetSessionTimer);
</script>
Code
An auto-advancing carousel needs a pause button and should stop while the user
is interacting with it. Here the interval is cleared on pause, hover, and focus,
and the control reflects its state with aria-pressed.
For any session or task time limit, let the user turn it off, adjust it, or
extend it — warn before it expires and offer a simple way to stay (2.2.1).
Give the warning enough lead time (at least 20 seconds is the standard
minimum) and announce it in a live region so it isn’t missed.
For auto-advancing carousels, tickers, and auto-updating content over five
seconds, provide a visible pause/stop control (2.2.2).
Stop auto-advance on hover and on focus so users can read a slide without it
moving out from under them.
Prefer not to auto-advance at all; let the user move between slides
themselves unless motion is essential.
Recap
Announce after-load updates through a live region that already exists in the
DOM: role="status" / aria-live="polite" for routine
messages, role="alert" for errors. Don’t move focus to do it (4.1.3).
Give every non-essential animation a prefers-reduced-motion: reduce
path that removes or shrinks the motion, and provide a pause control for anything
that moves on its own (2.3.3, 2.2.2).
Keep flashing under three flashes per second, below the general and red flash
thresholds — and never use flashing to draw attention (2.3.1).
Make time limits adjustable: let users turn them off, extend them, or pause an
auto-advancing carousel before the limit bites (2.2.1, 2.2.2).
The same fixes satisfy WCAG, EN 301 549, Section 508, and ADA
Title II at once — respect the user’s context and you meet them all.