Accessibility pattern · Media

Video player

Every control is a real button or slider with a name, so the player works by keyboard and screen reader, and nothing plays until someone presses Play. The captions and the transcript come from one cue list in the page, and a seek slider says "1 minute 5 seconds of 1 minute 20 seconds" instead of a bare number.

WCAG criteria
7
Keyboard rules
6
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.

Uttarayan: a day of kites in Ahmedabad

1 minute 20 seconds, with captions and a transcript. A demo clip drawn in your browser; its music starts only when you press Play.

Keys while the player has focus: K play or pause, M mute, C captions, F full screen.

Transcript

Described video: the rooftops of Ahmedabad's old city from dawn to after dark. The title reads "Uttarayan: Kites over Ahmedabad, 14 January". Kites in bright colors rise from the terraces; at 0:33 an orange kite is cut loose and drifts away. At dusk most kites come down, and a line of glowing lanterns climbs into the night.

  1. 0:00 [Wind over the rooftops]
  2. 0:05 14 January in Ahmedabad: Uttarayan starts at first light.
  3. 0:11 Families climb onto the terraces of the old city with their kites.
  4. 0:17 [Music: a light, plucked tune]
  5. 0:23 By mid-morning, the sky is full of paper kites.
  6. 0:29 The game is to cut a rival's string with your own.
  7. 0:33 [Shouting from a nearby roof] Kai po che!
  8. 0:37 It means "I have cut it", and you hear it all day.
  9. 0:45 The wind decides everything: when it drops, the kites sink.
  10. 0:53 [The wind picks up; the kites climb again]
  11. 1:00 At sunset, the sky over the old city turns orange.
  12. 1:07 After dark, paper lanterns called tukkal rise on the kite strings.
  13. 1:14 [The music fades]

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
Space or KPlays or pauses: Space on the play button, the seek slider or the volume; K anywhere in the player.
Arrow Right or Arrow LeftOn the seek slider, forward or back 5 seconds. On the volume slider, louder or quieter.
Page Up or Page DownOn the seek slider, forward or back 15 seconds.
Home or EndOn the seek slider, the start or the end of the video.
M or C or FAnywhere in the player: mute or unmute, captions on or off, full screen on or off.
Enter or Arrow DownOn the speed button, opens the menu on the current speed; arrows move, Enter chooses, Escape closes.

Screen readers

What it announces

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

WhenExpected announcement
Tab enters the playerUttarayan: a day of kites in Ahmedabad, group. Keys while the player has focus: K play or pause… Play, button
Tab reaches the seek sliderSeek, slider, 0 seconds of 1 minute 20 seconds
Arrow Right on the seek slider5 seconds
K while focus is on the volume sliderPlaying
M anywhere in the playerSound muted
Focus reaches the captions buttonCaptions, toggle button, pressed
The speed menu opens1×, menu item radio, checked, 3 of 6
The video reaches the endThe video has ended

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-video" data-ap-video-player>
  <div class="ap-video__player" role="group" aria-labelledby="vp-name" aria-describedby="vp-keys" data-vp-player>
    <div class="ap-video__screen" data-vp-screen>
      <video class="ap-video__media" playsinline preload="none" aria-labelledby="vp-name" data-vp-video></video>
      <canvas class="ap-video__media" width="640" height="360" role="img" aria-labelledby="vp-name" hidden data-vp-canvas></canvas>
      <span class="ap-video__hint" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M8 5.5v13l11-6.5z"/></svg></span>
      <p class="ap-video__cc" aria-hidden="true" hidden data-vp-cc><span></span></p>
    </div>

    <div class="ap-video__bar">
      <div class="ap-video__seekrow">
        <div class="ap-video__seek" role="slider" tabindex="0" aria-label="Seek" aria-valuemin="0" aria-valuemax="80" aria-valuenow="0" aria-valuetext="0 seconds of 1 minute 20 seconds" data-vp-seek>
          <span class="ap-video__rail"><span class="ap-video__fill"></span></span>
          <span class="ap-video__knob"></span>
        </div>
        <span class="ap-video__time" aria-hidden="true" translate="no"><span data-vp-now>0:00</span> / 1:20</span>
      </div>

      <div class="ap-video__row">
        <button type="button" class="ap-video__btn ap-video__btn--play" aria-label="Play" aria-keyshortcuts="K" data-vp-play>
          <svg class="ap-video__i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.5v13l11-6.5z"/></svg>
          <svg class="ap-video__i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5h3v14H8zM13 5h3v14h-3z"/></svg>
        </button>
        <div class="ap-video__sound">
          <button type="button" class="ap-video__btn ap-video__btn--mute" aria-pressed="false" aria-label="Mute" aria-keyshortcuts="M" data-vp-mute>
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-video__speaker" d="M11 5 6.5 9H3v6h3.5L11 19z"/><path class="ap-video__waves" d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/><path class="ap-video__cross" d="m16 9.5 5 5m0-5-5 5"/></svg>
          </button>
          <input class="ap-video__volume" type="range" min="0" max="100" step="5" value="80" aria-label="Volume level" aria-valuetext="80%" data-vp-volume />
        </div>
        <span class="ap-video__spacer"></span>
        <button type="button" class="ap-video__btn ap-video__btn--cc" aria-pressed="true" aria-label="Captions" aria-keyshortcuts="C" data-vp-cc-btn>
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="5.5" width="18" height="13" rx="2.5"/><path d="M10.5 10.2a2.2 2.2 0 1 0 0 3.6M17 10.2a2.2 2.2 0 1 0 0 3.6"/></svg>
        </button>
        <div class="ap-video__speed">
          <button type="button" class="ap-video__btn ap-video__btn--speed" id="vp-speed-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="vp-speed-menu" data-vp-speed><span class="ap-video__vh">Speed</span> <span data-vp-rate translate="no">1×</span></button>
          <div class="ap-video__menu" id="vp-speed-menu" role="menu" aria-labelledby="vp-speed-btn" hidden data-vp-menu>
            <button type="button" class="ap-video__item" role="menuitemradio" tabindex="-1" aria-checked="false" data-rate="0.5"><svg class="ap-video__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span translate="no">0.5×</span></button>
            <button type="button" class="ap-video__item" role="menuitemradio" tabindex="-1" aria-checked="false" data-rate="0.75"><svg class="ap-video__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span translate="no">0.75×</span></button>
            <button type="button" class="ap-video__item" role="menuitemradio" tabindex="-1" aria-checked="true" data-rate="1"><svg class="ap-video__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span translate="no">1×</span></button>
            <button type="button" class="ap-video__item" role="menuitemradio" tabindex="-1" aria-checked="false" data-rate="1.25"><svg class="ap-video__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span translate="no">1.25×</span></button>
            <button type="button" class="ap-video__item" role="menuitemradio" tabindex="-1" aria-checked="false" data-rate="1.5"><svg class="ap-video__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span translate="no">1.5×</span></button>
            <button type="button" class="ap-video__item" role="menuitemradio" tabindex="-1" aria-checked="false" data-rate="2"><svg class="ap-video__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span translate="no">2×</span></button>
          </div>
        </div>
        <button type="button" class="ap-video__btn ap-video__btn--fs" aria-label="Full screen" aria-keyshortcuts="F" data-vp-fs>
          <svg class="ap-video__i-enter" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg>
          <svg class="ap-video__i-exit" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg>
        </button>
      </div>
    </div>
  </div>

  <div class="ap-video__info">
    <h3 class="ap-video__name" id="vp-name">Uttarayan: a day of kites in Ahmedabad</h3>
    <p class="ap-video__meta">1 minute 20 seconds, with captions and a transcript. A demo clip drawn in your browser; its music starts only when you press Play.</p>
    <p class="ap-video__keys" id="vp-keys">Keys while the player has focus: <kbd>K</kbd> play or pause, <kbd>M</kbd> mute, <kbd>C</kbd> captions, <kbd>F</kbd> full screen.</p>
  </div>

  <details class="ap-video__transcript">
    <summary class="ap-video__toggle">
      <svg class="ap-video__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 6 6 6-6 6"/></svg>
      Transcript
    </summary>
    <div class="ap-video__tx">
      <p class="ap-video__described">Described video: the rooftops of Ahmedabad's old city from dawn to after dark. The title reads "Uttarayan: Kites over Ahmedabad, 14 January". Kites in bright colors rise from the terraces; at 0:33 an orange kite is cut loose and drifts away. At dusk most kites come down, and a line of glowing lanterns climbs into the night.</p>
      <ol class="ap-video__cues" data-vp-cues>
        <li data-start="0.5" data-end="4.5"><span class="ap-video__t" translate="no">0:00</span> <span data-vp-line>[Wind over the rooftops]</span></li>
        <li data-start="5" data-end="10.5"><span class="ap-video__t" translate="no">0:05</span> <span data-vp-line>14 January in Ahmedabad: Uttarayan starts at first light.</span></li>
        <li data-start="11" data-end="16.5"><span class="ap-video__t" translate="no">0:11</span> <span data-vp-line>Families climb onto the terraces of the old city with their kites.</span></li>
        <li data-start="17" data-end="22.5"><span class="ap-video__t" translate="no">0:17</span> <span data-vp-line>[Music: a light, plucked tune]</span></li>
        <li data-start="23" data-end="28.5"><span class="ap-video__t" translate="no">0:23</span> <span data-vp-line>By mid-morning, the sky is full of paper kites.</span></li>
        <li data-start="29" data-end="32.5"><span class="ap-video__t" translate="no">0:29</span> <span data-vp-line>The game is to cut a rival's string with your own.</span></li>
        <li data-start="33" data-end="37"><span class="ap-video__t" translate="no">0:33</span> <span data-vp-line>[Shouting from a nearby roof] Kai po che!</span></li>
        <li data-start="37.5" data-end="43.5"><span class="ap-video__t" translate="no">0:37</span> <span data-vp-line>It means "I have cut it", and you hear it all day.</span></li>
        <li data-start="45" data-end="51.5"><span class="ap-video__t" translate="no">0:45</span> <span data-vp-line>The wind decides everything: when it drops, the kites sink.</span></li>
        <li data-start="53" data-end="58.5"><span class="ap-video__t" translate="no">0:53</span> <span data-vp-line>[The wind picks up; the kites climb again]</span></li>
        <li data-start="60" data-end="66"><span class="ap-video__t" translate="no">1:00</span> <span data-vp-line>At sunset, the sky over the old city turns orange.</span></li>
        <li data-start="67" data-end="73"><span class="ap-video__t" translate="no">1:07</span> <span data-vp-line>After dark, paper lanterns called tukkal rise on the kite strings.</span></li>
        <li data-start="74" data-end="79.5"><span class="ap-video__t" translate="no">1:14</span> <span data-vp-line>[The music fades]</span></li>
      </ol>
    </div>
  </details>

  <p class="ap-video__vh" role="status" data-vp-status></p>
</div>

WCAG 2.2

What it meets

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

  • 1.2.2 Captions (Prerecorded) Level A

    Captions carry the narration and the sounds, drawn from cues in the page and on by default.

  • 1.2.3 Audio Description or Media Alternative (Prerecorded) Level A

    A described transcript gives the narration, the sounds and what the picture shows, for people who cannot see or hear it.

  • 1.4.2 Audio Control Level A

    Nothing plays on load. The music starts only from the Play button, and mute and volume are always in the bar.

  • 2.1.1 Keyboard Level A

    Every control is a button, a slider or a menu item reachable with Tab and the arrow keys.

  • 2.1.4 Character Key Shortcuts Level A

    The single-letter shortcuts work only while focus is inside the player, so they never fire while someone types elsewhere.

  • 4.1.2 Name, Role, Value Level A

    Play changes its name to Pause, mute and captions are toggle buttons with aria-pressed, and the seek slider has a value in words.

  • 4.1.3 Status Messages Level AA

    Changes made by a shortcut, and the end of the video, are announced in a status message.

Usage

When to use it

Use it

  • Video that people need to follow: tutorials, product demos, talks, news.
  • A player that must match a brand when the browser's own controls do not, with the same keyboard support.

Use something else

  • Short decorative loops: use a muted video with a visible pause button, as in the moving content pattern.
  • When the browser's controls are acceptable: the controls attribute gives keyboard support for free.
  • Audio only: use the audio player pattern, with chapters and a transcript.

Common failures

How it usually goes wrong

  • Autoplay with sound

    Sound that starts by itself talks over screen readers and startles people. Start muted, or better, wait for Play.

  • Icon buttons with no names

    Screen readers hear "button, button, button". Each control here has a name, and the toggles say whether they are pressed.

  • A seek bar that only says a number

    "65" means nothing. aria-valuetext gives minutes and seconds, and the total when the slider gains focus.

  • Controls that hide while playing

    Controls that fade out after a few seconds vanish for keyboard users mid-task. These stay visible.

  • Captions as part of the picture

    Open captions burned into the video cannot be turned off, restyled or translated. These are text drawn over it.

  • Global single-key shortcuts

    A page-wide M or K fires while people type or dictate. These keys work only inside the player.

Notes

Building it

  • The player talks to the media only through currentTime, play(), pause(), volume, muted and playbackRate and their events. With a real file, point it at the video element; the CanvasClip class only stands in for one here.
  • The seek slider's value is refreshed when it gains focus, when someone seeks and when playback stops. Updating a focused slider every second makes screen readers read the clock over the soundtrack.
  • Captions come from the transcript's cue list, which the script also turns into a WebVTT track for the video element; the player hides the browser's own rendering and draws its own so the captions sit above the controls.
  • Full screen goes to the whole player, not the video element, so the captions and the controls come along.
  • Captions are on by default here because the clip's narration exists only in its captions; in your own player, remember the person's last choice.

Sources: WAI-ARIA Authoring Practices: Media seek slider · W3C WAI: Media player accessibility · HTML: the track element

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