Dynamic content & motion

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.3 AA EN 301 549 Section 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

  1. 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.
  2. Update only the region’s text content. Don’t create the region and its message together, and don’t move focus to it.
  3. Keep messages short and self-contained; the user hears them out of context, so “3 results” beats a bare “3”.
  4. Reserve assertive announcements for genuine interruptions — overusing alert talks over everything else the user is doing.
  5. 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.3 AAA 2.2.2 A EN 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.

motion-pause.html
<button type="button" id="toggle" aria-pressed="false">Pause animation</button>
<div class="hero" id="hero"></div>

<style>
  /* Safe baseline for users who asked to reduce motion */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: .01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: .01ms !important;
    }
  }
  .hero.is-paused { animation-play-state: paused; }
</style>

<script>
  toggle.addEventListener('click', () => {
    const paused = hero.classList.toggle('is-paused');
    toggle.setAttribute('aria-pressed', String(paused));
    toggle.textContent = paused ? 'Play animation' : 'Pause animation';
  });
</script>

How to fix

  1. 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).
  2. Keep a small transform/opacity change if you need feedback; “reduce” means less motion, not necessarily none.
  3. 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.
  4. Avoid large-area parallax and zoom that tracks scroll; these are the effects most likely to provoke a vestibular reaction.
  5. Test with the OS “reduce motion” setting on, and confirm the pause control works by keyboard.

Flashing content

WCAG 2.2 · 2.3.1 A EN 301 549 Section 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.

good-flash.html
<p class="alert" role="alert">
  <svg aria-hidden="true" focusable="false" width="20" height="20">…</svg>
  <strong>Connection lost.</strong> Retrying…
</p>

<style>
  .alert { background:#fde8e8; color:#8a1f1f; } /* steady, no animation */
</style>

Code

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.

safe-pulse.css
.dot {
  /* 2s cycle = 0.5 flashes/sec; gentle 0.6–1 opacity, not on/off */
  animation: pulse 2s ease-in-out infinite;
}

@keyframes pulse {
  50% { opacity: 0.6; }
}

@media (prefers-reduced-motion: reduce) {
  .dot { animation: none; }
}

How to fix

  1. Never let anything flash more than three times in any one second — design the limit in, don’t test it out afterwards.
  2. Don’t use flashing to draw attention. Convey urgency with steady colour, an icon, text, and a live-region announcement instead.
  3. Treat saturated red flashing as off-limits; it carries a stricter threshold and the highest risk.
  4. Audit imported media too — autoplaying video, GIFs, and ads can flash even when your own CSS doesn’t.
  5. 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.1 A 2.2.2 A EN 301 549 ADA 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.

carousel-pause.html

How to fix

  1. 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).
  2. 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.
  3. For auto-advancing carousels, tickers, and auto-updating content over five seconds, provide a visible pause/stop control (2.2.2).
  4. Stop auto-advance on hover and on focus so users can read a slide without it moving out from under them.
  5. 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.