Accessibility pattern · Media

Moving content with pause

Anything that moves by itself for more than five seconds needs a Pause button placed before it, and sound should never start on its own. Here the background stops after five seconds anyway, the ticker holds while you point at it or tab into it, and nothing starts at all when the system asks for reduced motion.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Darjeeling · First flush

Picked this week in the hills

Spring tea from three estates, rolled by hand and packed within a week of picking.

Shop the first flush

Latest news

How to brew a cup

  1. Boil the water
  2. Add a spoon of leaves
  3. Steep for three minutes
  4. Pour and serve

Shop ambience

A tanpura drone for the tasting room. It plays only when you press Play.

  • Not this: sound that starts with the page talks over screen readers. Past 3 seconds it needs Stop or its own volume.
  • This: it waits for a press, and Stop and volume sit right beside it.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabReaches each part's Pause or Play button before the content it controls: the background, the ticker and its headlines, the animation, then the sound.
Enter or SpaceOn a Pause button, stops that motion where it is; the button becomes Play, which starts it again. Focus stays on the button.
Tab or ShiftTabInside the ticker, holds it still and brings the focused headline fully into view; it moves on when focus leaves.
Enter or SpaceOn the animation's Play, plays the four steps twice through and rests on the last one; on the sound's Play, starts the drone, and Stop ends it.
Arrow Left or Arrow RightOn Ambience volume, lowers or raises the volume in steps of 5; Home and End set it to 0 or 100.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the background's buttonPicked this week in the hills, region. Pause background animation, button
The background stops by itself after five secondsNothing is said; the button is renamed Play background animation, read when focus next reaches it
Pause is pressed on the tickerPlay news ticker, button
Focus reaches a headline09:40 Monsoon blend is back in stock, link
Play is pressed on the animationPause animation, button
Play is pressed on the soundStop the ambience, button (and the drone starts)
The volume changesAmbience volume, slider, 60

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-autoplay-media" data-ap-autoplay-media>
  <p class="ap-autoplay-media__calm" data-apm-reduced hidden>
    <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg>
    Reduced motion is on: nothing moves until you press Play.
  </p>

  <section class="ap-autoplay-media__hero" aria-labelledby="apm-hero-name">
    <button type="button" class="ap-autoplay-media__toggle is-playing" data-apm-hero aria-label="Pause background animation">
      <span class="ap-autoplay-media__glyph">
        <svg class="ap-autoplay-media__icon ap-autoplay-media__icon--pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"/></svg>
        <svg class="ap-autoplay-media__icon ap-autoplay-media__icon--play" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.2v13.6L19 12Z"/></svg>
        <svg class="ap-autoplay-media__ring" viewBox="0 0 32 32" aria-hidden="true" focusable="false"><circle cx="16" cy="16" r="14.5" pathLength="100"/></svg>
      </span>
      <span class="ap-autoplay-media__word">Pause</span>
    </button>
    <canvas class="ap-autoplay-media__canvas" data-apm-canvas aria-hidden="true"></canvas>
    <div class="ap-autoplay-media__copy">
      <p class="ap-autoplay-media__kicker">Darjeeling · First flush</p>
      <h3 class="ap-autoplay-media__name" id="apm-hero-name">Picked this week in the hills</h3>
      <p class="ap-autoplay-media__text">Spring tea from three estates, rolled by hand and packed within a week of picking.</p>
      <a class="ap-btn ap-btn--primary" href="#apm-hero-name">Shop the first flush</a>
    </div>
  </section>

  <section class="ap-autoplay-media__ticker" aria-labelledby="apm-ticker-name">
    <h3 class="ap-autoplay-media__tag" id="apm-ticker-name">Latest news</h3>
    <button type="button" class="ap-autoplay-media__toggle is-playing" data-apm-ticker aria-label="Pause news ticker">
      <span class="ap-autoplay-media__glyph">
        <svg class="ap-autoplay-media__icon ap-autoplay-media__icon--pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"/></svg>
        <svg class="ap-autoplay-media__icon ap-autoplay-media__icon--play" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.2v13.6L19 12Z"/></svg>
      </span>
      <span class="ap-autoplay-media__word">Pause</span>
    </button>
    <div class="ap-autoplay-media__strip" data-apm-strip>
      <div class="ap-autoplay-media__rail">
        <ul class="ap-autoplay-media__news">
          <li><a href="#apm-ticker-name"><time datetime="09:40">09:40</time> <span>Monsoon blend is back in stock</span></a></li>
          <li><a href="#apm-ticker-name"><time datetime="10:15">10:15</time> <span>Free delivery in Pune and Mumbai this week</span></a></li>
          <li><a href="#apm-ticker-name"><time datetime="11:30">11:30</time> <span>Tasting in Indiranagar on Saturday at 5 pm</span></a></li>
          <li><a href="#apm-ticker-name"><time datetime="12:05">12:05</time> <span>New: cardamom and saffron chai</span></a></li>
          <li><a href="#apm-ticker-name"><time datetime="13:20">13:20</time> <span>Gift boxes now ship in two days</span></a></li>
        </ul>
      </div>
    </div>
  </section>

  <div class="ap-autoplay-media__pair">
    <section class="ap-autoplay-media__card" aria-labelledby="apm-gif-name">
      <h3 class="ap-autoplay-media__label" id="apm-gif-name">How to brew a cup</h3>
      <figure class="ap-autoplay-media__figure">
        <div class="ap-autoplay-media__screen">
          <svg class="ap-autoplay-media__art" viewBox="0 0 240 150" role="img" aria-label="Animation of the four steps below" focusable="false">
            <path class="ap-autoplay-media__table" d="M16 128h208"/>
            <g class="ap-autoplay-media__cel is-shown" data-apm-cel>
              <rect class="ap-autoplay-media__plate" x="70" y="114" width="100" height="10" rx="3"/>
              <path class="ap-autoplay-media__warm" d="M96 114c2-7 5-8 6-14 2 6 5 7 6 14Zm16 0c2-7 5-8 6-14 2 6 5 7 6 14Zm16 0c2-7 5-8 6-14 2 6 5 7 6 14Z"/>
              <path class="ap-autoplay-media__ink" d="M86 104c-4-24 8-40 34-40s38 16 34 40Z"/>
              <path class="ap-autoplay-media__ink" d="M150 86l22-16 5 6-21 18Z"/>
              <path class="ap-autoplay-media__line" d="M98 66c0-22 44-22 44 0"/>
              <path class="ap-autoplay-media__ink ap-autoplay-media__ink--deep" d="M108 64c0-5 5-8 12-8s12 3 12 8Z"/>
              <path class="ap-autoplay-media__steam" d="M180 62c-6-6 4-10-2-16M192 66c-6-6 4-10-2-16"/>
            </g>
            <g class="ap-autoplay-media__cel" data-apm-cel>
              <path class="ap-autoplay-media__ink" d="M78 124c-10-34 10-56 42-56s52 22 42 56Z"/>
              <path class="ap-autoplay-media__ink" d="M158 100c14-2 20-12 26-24l6 4c-6 16-14 26-30 30Z"/>
              <path class="ap-autoplay-media__line" d="M80 84c-18 0-20 28 0 30"/>
              <ellipse class="ap-autoplay-media__hole" cx="120" cy="70" rx="22" ry="5"/>
              <path class="ap-autoplay-media__line" d="M178 22 142 44"/>
              <ellipse class="ap-autoplay-media__ink ap-autoplay-media__ink--deep" cx="136" cy="48" rx="10" ry="5" transform="rotate(-31 136 48)"/>
              <path class="ap-autoplay-media__leaf" d="M116 50c4-5 11-5 13 0-4 5-11 5-13 0ZM104 60c4-5 11-5 13 0-4 5-11 5-13 0ZM122 62c4-5 11-5 13 0-4 5-11 5-13 0Z"/>
            </g>
            <g class="ap-autoplay-media__cel" data-apm-cel>
              <path class="ap-autoplay-media__ink" d="M78 124c-10-34 10-56 42-56s52 22 42 56Z"/>
            <path class="ap-autoplay-media__ink" d="M158 100c14-2 20-12 26-24l6 4c-6 16-14 26-30 30Z"/>
            <path class="ap-autoplay-media__line" d="M80 84c-18 0-20 28 0 30"/>
            <path class="ap-autoplay-media__ink ap-autoplay-media__ink--deep" d="M100 70c0-8 9-12 20-12s20 4 20 12Z"/>
            <circle class="ap-autoplay-media__ink ap-autoplay-media__ink--deep" cx="120" cy="54" r="4"/>
            <path class="ap-autoplay-media__band" d="M84 98h72"/>
              <path class="ap-autoplay-media__steam" d="M112 44c-6-6 4-10-2-16M128 44c-6-6 4-10-2-16"/>
              <circle class="ap-autoplay-media__face" cx="210" cy="64" r="15"/>
              <path class="ap-autoplay-media__warm" d="M210 64V49a15 15 0 1 1-8.82 27.14Z"/>
              <circle class="ap-autoplay-media__line" cx="210" cy="64" r="15"/>
              <path class="ap-autoplay-media__line" d="M206 44h8"/>
            </g>
            <g class="ap-autoplay-media__cel" data-apm-cel>
              <g transform="translate(-36 -8) rotate(24 164 124)">
                <path class="ap-autoplay-media__ink" d="M78 124c-10-34 10-56 42-56s52 22 42 56Z"/>
            <path class="ap-autoplay-media__ink" d="M158 100c14-2 20-12 26-24l6 4c-6 16-14 26-30 30Z"/>
            <path class="ap-autoplay-media__line" d="M80 84c-18 0-20 28 0 30"/>
            <path class="ap-autoplay-media__ink ap-autoplay-media__ink--deep" d="M100 70c0-8 9-12 20-12s20 4 20 12Z"/>
            <circle class="ap-autoplay-media__ink ap-autoplay-media__ink--deep" cx="120" cy="54" r="4"/>
            <path class="ap-autoplay-media__band" d="M84 98h72"/>
              </g>
              <path class="ap-autoplay-media__pour" d="M171 82c9 4 16 10 21 24"/>
              <path class="ap-autoplay-media__ink" d="M176 104h40v6c0 12-8 18-20 18s-20-6-20-18Z"/>
              <path class="ap-autoplay-media__line" d="M216 108c9 0 9 12 0 12"/>
              <path class="ap-autoplay-media__warm" d="M180 106h32v3c0 3-2 4-4 4h-24c-2 0-4-1-4-4Z"/>
              <path class="ap-autoplay-media__steam" d="M192 96c-6-6 4-10-2-16M204 96c-6-6 4-10-2-16"/>
            </g>
          </svg>
          <span class="ap-autoplay-media__badge" aria-hidden="true" translate="no">GIF</span>
          <button type="button" class="ap-autoplay-media__toggle" data-apm-gif aria-label="Play animation">
            <span class="ap-autoplay-media__glyph">
              <svg class="ap-autoplay-media__icon ap-autoplay-media__icon--pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"/></svg>
        <svg class="ap-autoplay-media__icon ap-autoplay-media__icon--play" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.2v13.6L19 12Z"/></svg>
            </span>
            <span class="ap-autoplay-media__word">Play</span>
          </button>
        </div>
        <figcaption>
          <ol class="ap-autoplay-media__steps">
            <li class="is-now" data-apm-step>Boil the water</li>
            <li data-apm-step>Add a spoon of leaves</li>
            <li data-apm-step>Steep for three minutes</li>
            <li data-apm-step>Pour and serve</li>
          </ol>
        </figcaption>
      </figure>
    </section>

    <section class="ap-autoplay-media__card" aria-labelledby="apm-sound-name">
      <h3 class="ap-autoplay-media__label" id="apm-sound-name">Shop ambience</h3>
      <p class="ap-autoplay-media__note">A tanpura drone for the tasting room. It plays only when you press Play.</p>
      <div class="ap-autoplay-media__player">
        <button type="button" class="ap-autoplay-media__toggle" data-apm-sound aria-label="Play the ambience">
          <span class="ap-autoplay-media__glyph">
            <svg class="ap-autoplay-media__icon ap-autoplay-media__icon--pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="6.5" y="6.5" width="11" height="11" rx="2"/></svg>
            <svg class="ap-autoplay-media__icon ap-autoplay-media__icon--play" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.2v13.6L19 12Z"/></svg>
          </span>
          <span class="ap-autoplay-media__word">Play</span>
        </button>
        <span class="ap-autoplay-media__bars" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span></span>
      </div>
      <div class="ap-autoplay-media__volume">
        <label class="ap-label" for="apm-volume">Ambience volume</label>
        <div class="ap-autoplay-media__range">
          <input type="range" id="apm-volume" min="0" max="100" step="5" value="40" data-apm-volume />
          <span class="ap-autoplay-media__level" data-apm-level aria-hidden="true">40%</span>
        </div>
      </div>
      <ul class="ap-autoplay-media__rules">
        <li class="ap-autoplay-media__rule ap-autoplay-media__rule--no">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
          <span><strong>Not this:</strong> sound that starts with the page talks over screen readers. Past 3 seconds it needs Stop or its own volume.</span>
        </li>
        <li class="ap-autoplay-media__rule ap-autoplay-media__rule--yes">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
          <span><strong>This:</strong> it waits for a press, and Stop and volume sit right beside it.</span>
        </li>
      </ul>
    </section>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 2.2.2 Pause, Stop, Hide Level A

    The background stops by itself within five seconds and has Pause anyway; the ticker has Pause and holds on hover and focus; the animation moves only when asked.

  • 1.4.2 Audio Control Level A

    Nothing plays on load. The drone starts on a press, Stop is the same button, and its own volume works apart from the system volume.

  • 1.1.1 Non-text Content Level A

    The background canvas is hidden as decoration; the animation has a name, and the steps it shows are written out under it.

  • 2.3.3 Animation from Interactions Level AAA

    Under reduced motion, real or simulated, nothing moves by itself, and the countdown ring and sound bars stay still.

  • 2.1.1 Keyboard Level A

    Every Pause, Play and Stop is a button, the volume is a native slider, and focus inside the ticker holds it.

  • 2.4.3 Focus Order Level A

    Each Pause control comes before the moving content it controls, so nobody has to tab through motion to stop it.

  • 4.1.2 Name, Role, Value Level A

    Each button's name says what it will do next and changes when it is pressed, so its state is never shown by an icon alone.

Usage

When to use it

Use it

  • Decorative motion that adds something, like a hero loop, a ticker or an animated illustration, as long as it can be stopped.
  • Sound people choose to hear: a preview, an ambience, the pronunciation of a word.

Use something else

  • Moving text people must read in time: show it as a list, or let people step through it.
  • Sound on page load, of any length: let people press Play.
  • Motion on every page of a product: once is a delight, every visit is a cost.

Common failures

How it usually goes wrong

  • Pause only on hover

    Keyboard and touch users cannot hover. Hover and focus hold the ticker here, but the Pause button is what everyone can rely on.

  • The stop control at the end

    If Pause comes after the moving content, keyboard users tab through motion to reach it. Each one here is the first control in its part.

  • Sound that starts by itself

    Autoplaying sound talks over a screen reader, which also speaks through the speakers. Past 3 seconds it fails 1.4.2 without a stop or its own volume.

  • An animated GIF with no controls

    Browsers cannot pause a GIF. Show a still with a Play button, and use an SVG, a canvas or a video that script can stop.

  • A ticker in a live region

    Announcing every headline as it slides past interrupts whatever someone is reading. The ticker here is a plain list of links.

  • Ignoring reduced motion

    Large moving backgrounds can cause nausea and dizziness. When the system asks for less motion, nothing here starts by itself.

Notes

Building it

  • Each kind of motion stops in its own way: cancelAnimationFrame for a canvas loop, a class or animation-play-state for CSS, pause() for a video. Keep the state in one place so the button and the automatic stop agree.
  • Reduced motion changes what starts by itself, not what people ask for: everything starts still, and Play still plays.
  • The ticker loops by showing a copy of its list. The copy is aria-hidden and its links are out of the Tab order, so every headline is read and reached once.
  • Browsers allow sound only after a press, so the AudioContext is created in the click handler, and the master gain carries the page's own volume.
  • The option above sets data-reduce-motion on the root, which the script and the stylesheet treat exactly like prefers-reduced-motion: reduce.

Sources: Understanding WCAG 2.2: Pause, Stop, Hide · Understanding WCAG 2.2: Audio Control · MDN: prefers-reduced-motion

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction