Accessibility pattern · Media

Audio player

Real buttons, a select and a slider make every control reachable by keyboard and named for screen readers, and no sound starts until someone presses Play. Chapters jump to their start and mark the one playing with aria-current; the transcript highlights the line being spoken without announcing it over the audio.

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

The Raag Clock · Episode 14

Four raags round the clock

With Meera Iyer · 2 minutes 20 seconds

The music is made in your browser when you press Play; Meera's words are in the transcript.

Chapters

Transcript

  1. 0:00 [Music: a tanpura drone, tuned to Sa and Pa]
  2. 0:05 Meera: Welcome to The Raag Clock. I'm Meera Iyer.
  3. 0:10 Meera: In Hindustani music, many raags belong to a time of day. Today we go round the clock in four short phrases.
  4. 0:20 [Music: Bhairav, a slow, low phrase]
  5. 0:28 Meera: Bhairav is sung at dawn. Its flattened second and sixth notes give it a grave, prayerful sound.
  6. 0:40 [The phrase climbs to the upper Sa, then settles back]
  7. 0:50 [Music: Bhimpalasi, a brighter, swaying phrase]
  8. 0:58 Meera: Bhimpalasi is for the hot afternoon. It leans on the flattened third and seventh.
  9. 1:08 [The melody lingers on Ma, then falls home to Sa]
  10. 1:20 [Music: Yaman, an open, rising phrase]
  11. 1:28 Meera: Yaman opens the night. Its sharp fourth, tivra Ma, makes it sound bright and searching.
  12. 1:40 [Long notes climb to the upper Re and rest on Sa]
  13. 1:50 [Music: Malkauns, a deep, five-note phrase]
  14. 1:58 Meera: Malkauns belongs to midnight. It uses only five notes and leaves out Re and Pa.
  15. 2:08 Meera: That's the clock. Next week: the monsoon raags.
  16. 2:14 [The tanpura fades out]

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
Enter or SpaceOn Play, plays or pauses; on Back 15 seconds or Forward 15 seconds, skips; on a chapter, goes to its start.
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 episode.
SpaceOn the seek slider, plays or pauses, and says which.
Arrow Down or Arrow UpOn the speed select, chooses a slower or faster speed.

Screen readers

What it announces

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

WhenExpected announcement
Tab enters the playerFour raags round the clock, group. Seek, slider, 0 seconds of 2 minutes 20 seconds
Focus reaches PlayPlay, button
Page Up on the seek slider15 seconds
Space on the seek sliderPlaying
Focus reaches the chapter playing now0:20 Bhairav, at dawn, button, current
The transcript moves to the next lineNothing: the highlight is visual only
The episode endsEpisode finished

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-audio" data-ap-audio-player>
  <div class="ap-audio__player" role="group" aria-labelledby="aud-name" data-aud-player>
    <div class="ap-audio__top">
      <svg class="ap-audio__cover" viewBox="0 0 96 96" aria-hidden="true" focusable="false">
        <rect width="96" height="96" rx="14" fill="#1E1B4B"/>
        <path d="M48 18a30 30 0 0 1 30 30H48z" fill="#F59E0B"/>
        <path d="M78 48a30 30 0 0 1-30 30V48z" fill="#FDE68A"/>
        <path d="M48 78a30 30 0 0 1-30-30h30z" fill="#F472B6"/>
        <path d="M18 48a30 30 0 0 1 30-30v30z" fill="#6366F1"/>
        <circle cx="48" cy="48" r="17" fill="#1E1B4B"/>
        <path d="M48 48V36M48 48l8 5" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
        <circle cx="48" cy="48" r="2.6" fill="#FFFFFF"/>
      </svg>
      <div class="ap-audio__id">
        <p class="ap-audio__show">The Raag Clock · Episode 14</p>
        <h3 class="ap-audio__name" id="aud-name">Four raags round the clock</h3>
        <p class="ap-audio__by">With Meera Iyer · 2 minutes 20 seconds</p>
      </div>
    </div>

    <div class="ap-audio__seekrow">
      <span class="ap-audio__time" aria-hidden="true" translate="no" data-aud-now>0:00</span>
      <div class="ap-audio__seek" role="slider" tabindex="0" aria-label="Seek" aria-valuemin="0" aria-valuemax="140" aria-valuenow="0" aria-valuetext="0 seconds of 2 minutes 20 seconds" data-aud-seek>
        <span class="ap-audio__rail"><span class="ap-audio__fill"></span></span>
        <span class="ap-audio__marks" aria-hidden="true" data-aud-marks></span>
        <span class="ap-audio__knob"></span>
      </div>
      <span class="ap-audio__time" aria-hidden="true" translate="no">2:20</span>
    </div>

    <div class="ap-audio__controls">
      <div class="ap-audio__rate">
        <label class="ap-audio__rate-label" for="aud-rate">Speed</label>
        <select class="ap-audio__select" id="aud-rate" data-aud-rate>
          <option value="0.75">0.75×</option>
          <option value="1" selected>1×</option>
          <option value="1.25">1.25×</option>
          <option value="1.5">1.5×</option>
          <option value="2">2×</option>
        </select>
      </div>
      <div class="ap-audio__transport">
        <button type="button" class="ap-audio__btn ap-audio__btn--skip" aria-label="Back 15 seconds" data-aud-skip="-15"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4.5 12a7.5 7.5 0 1 0 2.2-5.3M4.5 4v4.5H9"/><text x="12" y="15.4" text-anchor="middle">15</text></svg></button>
        <button type="button" class="ap-audio__btn ap-audio__btn--play" aria-label="Play" data-aud-play>
          <svg class="ap-audio__i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.5v13l11-6.5z"/></svg>
          <svg class="ap-audio__i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7.5 5h3.2v14H7.5zM13.3 5h3.2v14h-3.2z"/></svg>
        </button>
        <button type="button" class="ap-audio__btn ap-audio__btn--skip" aria-label="Forward 15 seconds" data-aud-skip="15"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4v4.5H15"/><text x="12" y="15.4" text-anchor="middle">15</text></svg></button>
      </div>
      <div class="ap-audio__sound">
        <button type="button" class="ap-audio__btn ap-audio__btn--mute" aria-pressed="false" aria-label="Mute" data-aud-mute>
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M11 5 6.5 9H3v6h3.5L11 19z"/><path class="ap-audio__waves" d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/><path class="ap-audio__cross" d="m16 9.5 5 5m0-5-5 5"/></svg>
        </button>
        <input class="ap-audio__volume" type="range" min="0" max="100" step="5" value="80" aria-label="Volume level" aria-valuetext="80%" data-aud-volume />
      </div>
    </div>
    <p class="ap-audio__note">The music is made in your browser when you press Play; Meera's words are in the transcript.</p>
  </div>

  <div class="ap-audio__more">
    <div class="ap-audio__panel">
      <h4 class="ap-audio__h" id="aud-chapters">Chapters</h4>
      <ol class="ap-audio__chapters" aria-labelledby="aud-chapters">
          <li><button type="button" class="ap-audio__chapter" data-aud-chapter data-at="0" aria-current="true"><span class="ap-audio__ct" translate="no">0:00</span><span class="ap-audio__cn">Tuning up</span><svg class="ap-audio__eq" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 13V7M8 13V3M13 13V9"/></svg></button></li>
          <li><button type="button" class="ap-audio__chapter" data-aud-chapter data-at="20"><span class="ap-audio__ct" translate="no">0:20</span><span class="ap-audio__cn">Bhairav, at dawn</span><svg class="ap-audio__eq" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 13V7M8 13V3M13 13V9"/></svg></button></li>
          <li><button type="button" class="ap-audio__chapter" data-aud-chapter data-at="50"><span class="ap-audio__ct" translate="no">0:50</span><span class="ap-audio__cn">Bhimpalasi, in the afternoon</span><svg class="ap-audio__eq" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 13V7M8 13V3M13 13V9"/></svg></button></li>
          <li><button type="button" class="ap-audio__chapter" data-aud-chapter data-at="80"><span class="ap-audio__ct" translate="no">1:20</span><span class="ap-audio__cn">Yaman, at dusk</span><svg class="ap-audio__eq" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 13V7M8 13V3M13 13V9"/></svg></button></li>
          <li><button type="button" class="ap-audio__chapter" data-aud-chapter data-at="110"><span class="ap-audio__ct" translate="no">1:50</span><span class="ap-audio__cn">Malkauns, at midnight</span><svg class="ap-audio__eq" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 13V7M8 13V3M13 13V9"/></svg></button></li>
      </ol>
    </div>
    <div class="ap-audio__panel">
      <h4 class="ap-audio__h" id="aud-transcript">Transcript</h4>
      <div class="ap-audio__script" role="region" aria-labelledby="aud-transcript" tabindex="0" data-aud-transcript>
        <ol class="ap-audio__lines">
            <li data-start="0" data-end="5"><span class="ap-audio__lt" translate="no">0:00</span> <span class="ap-audio__line">[Music: a tanpura drone, tuned to Sa and Pa]</span></li>
            <li data-start="5" data-end="10"><span class="ap-audio__lt" translate="no">0:05</span> <span class="ap-audio__line"><b>Meera:</b> Welcome to The Raag Clock. I'm Meera Iyer.</span></li>
            <li data-start="10" data-end="20"><span class="ap-audio__lt" translate="no">0:10</span> <span class="ap-audio__line"><b>Meera:</b> In Hindustani music, many raags belong to a time of day. Today we go round the clock in four short phrases.</span></li>
            <li data-start="20" data-end="28"><span class="ap-audio__lt" translate="no">0:20</span> <span class="ap-audio__line">[Music: Bhairav, a slow, low phrase]</span></li>
            <li data-start="28" data-end="40"><span class="ap-audio__lt" translate="no">0:28</span> <span class="ap-audio__line"><b>Meera:</b> Bhairav is sung at dawn. Its flattened second and sixth notes give it a grave, prayerful sound.</span></li>
            <li data-start="40" data-end="50"><span class="ap-audio__lt" translate="no">0:40</span> <span class="ap-audio__line">[The phrase climbs to the upper Sa, then settles back]</span></li>
            <li data-start="50" data-end="58"><span class="ap-audio__lt" translate="no">0:50</span> <span class="ap-audio__line">[Music: Bhimpalasi, a brighter, swaying phrase]</span></li>
            <li data-start="58" data-end="68"><span class="ap-audio__lt" translate="no">0:58</span> <span class="ap-audio__line"><b>Meera:</b> Bhimpalasi is for the hot afternoon. It leans on the flattened third and seventh.</span></li>
            <li data-start="68" data-end="80"><span class="ap-audio__lt" translate="no">1:08</span> <span class="ap-audio__line">[The melody lingers on Ma, then falls home to Sa]</span></li>
            <li data-start="80" data-end="88"><span class="ap-audio__lt" translate="no">1:20</span> <span class="ap-audio__line">[Music: Yaman, an open, rising phrase]</span></li>
            <li data-start="88" data-end="100"><span class="ap-audio__lt" translate="no">1:28</span> <span class="ap-audio__line"><b>Meera:</b> Yaman opens the night. Its sharp fourth, tivra Ma, makes it sound bright and searching.</span></li>
            <li data-start="100" data-end="110"><span class="ap-audio__lt" translate="no">1:40</span> <span class="ap-audio__line">[Long notes climb to the upper Re and rest on Sa]</span></li>
            <li data-start="110" data-end="118"><span class="ap-audio__lt" translate="no">1:50</span> <span class="ap-audio__line">[Music: Malkauns, a deep, five-note phrase]</span></li>
            <li data-start="118" data-end="128"><span class="ap-audio__lt" translate="no">1:58</span> <span class="ap-audio__line"><b>Meera:</b> Malkauns belongs to midnight. It uses only five notes and leaves out Re and Pa.</span></li>
            <li data-start="128" data-end="134"><span class="ap-audio__lt" translate="no">2:08</span> <span class="ap-audio__line"><b>Meera:</b> That's the clock. Next week: the monsoon raags.</span></li>
            <li data-start="134" data-end="140"><span class="ap-audio__lt" translate="no">2:14</span> <span class="ap-audio__line">[The tanpura fades out]</span></li>
        </ol>
      </div>
    </div>
  </div>

  <p class="ap-audio__vh" role="status" data-aud-status></p>
</div>

WCAG 2.2

What it meets

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

  • 1.2.1 Audio-only and Video-only (Prerecorded) Level A

    A full transcript gives the words and describes the music, for people who cannot hear the episode.

  • 1.4.2 Audio Control Level A

    Nothing plays on load: the AudioContext is created by the first press of Play, and Mute and volume are always beside it.

  • 2.1.1 Keyboard Level A

    Every control is a button, a select or a slider, and the seek slider answers the arrow keys, Page Up, Page Down, Home and End.

  • 2.2.2 Pause, Stop, Hide Level A

    The transcript moves only while the episode plays, which Pause stops, and never while the pointer, focus or a scroll is in it.

  • 4.1.2 Name, Role, Value Level A

    Play changes its name to Pause, Mute has aria-pressed, the playing chapter has aria-current, and the seek slider has a value in words.

  • 4.1.3 Status Messages Level AA

    Play and pause from the slider, and the end of the episode, are announced in a status message without moving focus.

Usage

When to use it

Use it

  • Podcasts, interviews, recorded talks and lessons, especially long ones that need chapters.
  • Audio with speech: the transcript is the alternative people who cannot hear it rely on.

Use something else

  • A short sound effect or sample: a native audio element with controls is enough.
  • Background music: don't add it, or start it muted with a visible pause, as in the moving content pattern.
  • Video: use the video player pattern, which adds captions.

Common failures

How it usually goes wrong

  • Audio that starts on load

    Sound that plays by itself drowns out screen readers. Under 1.4.2 anything longer than three seconds needs a pause or a volume control, and silence until asked is better still.

  • A transcript that announces every line

    A live region on the transcript reads each line aloud over the audio itself. Highlight the current line visually and leave the region quiet.

  • A transcript that fights the reader

    Auto-scrolling while someone reads further up yanks the page from under them. This one waits while the pointer, focus or a scroll is in it.

  • Chapters that are not controls

    Times written as plain text cannot be activated. Each chapter here is a button that seeks, and the playing one carries aria-current.

  • Skip buttons named by their icon

    "15" or a curved arrow says nothing. "Back 15 seconds" and "Forward 15 seconds" do.

Notes

Building it

  • The player uses only the media element's interface: currentTime, play(), pause(), volume, muted, playbackRate and their events. With a real file, point it at an audio element; Episode only stands in for one here.
  • Create the AudioContext inside the click or key handler that starts playback. Browsers refuse to start one without a user gesture anyway, and it keeps 1.4.2 honest.
  • aria-current marks the playing chapter for anyone who reaches it, but a change of aria-current on another element is not announced, which is right while audio plays.
  • The transcript region has tabindex="0" so keyboard users can scroll it, and a name from its heading.
  • Speed is a native select: it has the arrow keys, the type-ahead and the screen reader support for free.

Sources: WAI-ARIA Authoring Practices: Media seek slider · W3C WAI: Transcripts · MDN: Web Audio API best practices

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