# Audio description with no gap to say what is shown

Source: https://easeweb.dev/learn/extended-audio-description
Topics: Images and media > Videos pause when there is no gap to describe what is shown (WCAG 1.2.7)
The fix: Either publish an extended version, in which the picture holds on the card while its description plays, and offer it from the audio description button, or give the player the descriptions as text and have it pause the video while it reads each one aloud. The second works only where the viewer's browser has a voice for the language.
Test with: screen off, keyboard, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/extended-audio-description-prerecorded.html https://www.w3.org/WAI/WCAG22/Techniques/general/G8 https://www.w3.org/WAI/WCAG22/Techniques/html/H96 https://www.w3.org/WAI/media/av/description/ https://github.com/ableplayer/ableplayer https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis https://developer.mozilla.org/en-US/docs/Web/API/TextTrack/cuechange_event

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

The coffee roaster's brewing video now has a described version, offered from an Audio description button under the player. Then the shop added a talk-through version, with the roaster explaining as the video goes. Over the first card, 15 g coffee and 250 ml water, the roaster says "Weigh it out like this" and stops, and the describer reads the amounts in the pause. Over the next two cards the roaster talks straight through: "Then let the water rest a moment. Straight off the boil, it scorches the grounds", and then "Pour slowly, and stop here. Keep the stream thin, and let the grounds settle as you go." The cards say "Cool 30 s after boiling" and "Pour in 3 stages, stop at 3:00", and each is gone before the roaster draws breath. The describer had no gap to speak in, so the described version says nothing about them. Someone listening without the picture is told how much coffee to use, and never how long to wait or when to stop.

## Why it fails

Success criterion 1.2.7, at Level AAA, covers the case where the pauses in a video's speech are too short for its audio description to convey the sense of the video. Then it asks for extended audio description: the video is paused, or its picture frozen, while the description plays, and then it carries on. That is the case here. After the first card, the longest gap in the narration is under a second, and the description of each card takes two to four seconds to say: speeding the describer up, or cutting the description to "A kettle", would lose the time and the amounts the card is there to give. The described version does the best a pause-only description can, and stops short. Nothing in the page makes more room: the video never holds, and no other version is offered. The browser has no way to do it either. The `video` element plays one soundtrack at its own pace, and a description has to be written into the file, or played by a script that pauses the video, for the picture to wait.

## Who is affected

Anyone who follows the video by ear meets the same gap, such as someone with their eyes on the kettle. A talk-through narration is often good to listen to and full of useful detail; what it costs is the room a describer needs.

## What automation and AI miss

axe has no check for audio description. EaseWeb's audit lists every video for a person to review and notes whether an audio description control or a descriptions track sits beside it, so the described version here is found, and looks like a pass. Whether its descriptions fit, and what they leave out, can only be heard: a scanner can't compare the picture with the soundtrack, or measure the gaps in the speech against what still needs saying. AI tools can find the silences in a soundtrack and draft descriptions from frames, but a person who knows the content still has to decide what the viewer would miss and where the picture should wait.

## Before: a described version that runs out of pauses

```html
<video id="brew-video" controls preload="metadata"
  src="brew-talk.webm" poster="brew-talk.jpg"
  data-described="brew-talk.described.webm">
  <track kind="captions" srclang="en" label="English"
    src="brew-talk.en.vtt" data-described="brew-talk.described.en.vtt">
</video>
<button type="button" id="describe" aria-pressed="false">
  Audio description
</button>

<script>
  const video = document.getElementById('brew-video');
  const track = video.querySelector('track');
  const plain = { video: video.getAttribute('src'), captions: track.getAttribute('src') };
  document.getElementById('describe').addEventListener('click', (event) => {
    const button = event.currentTarget;
    const on = button.getAttribute('aria-pressed') !== 'true';
    const { currentTime, paused, playbackRate } = video;
    const captions = track.track.mode;
    video.src = on ? video.dataset.described : plain.video;
    track.src = on ? track.dataset.described : plain.captions;
    // Carry on from the same moment, at the same speed, with the captions as they were.
    video.addEventListener('loadedmetadata', () => {
      video.currentTime = currentTime;
      video.playbackRate = playbackRate;
      track.track.mode = captions;
      if (!paused) video.play();
    }, { once: true });
    button.setAttribute('aria-pressed', String(on));
  });
</script>
```

```vtt brew-talk.described.en.vtt
WEBVTT

00:04.200 --> 00:05.674
Narrator: Weigh it out like this.

00:05.724 --> 00:09.950
Describer: 15 grams of coffee, 250 millilitres of water.

00:10.200 --> 00:14.690
Narrator: Then let the water rest a moment.
Straight off the boil, it scorches the grounds.

00:15.200 --> 00:20.150
Narrator: Pour slowly, and stop here.
Keep the stream thin, and let the grounds settle as you go.

00:20.300 --> 00:22.300
[Kitchen timer rings]
```

The button and the switch are right, and the first description is too. The captions file of the described version shows where it stops:

- After the first card, each line of narration starts within about half a second of the last one ending, so there is no gap long enough for a description.
- So the described version never says "Cool 30 s after boiling", or "Pour in 3 stages, stop at 3:00".
- The video never holds to make room, and no extended version is offered.

## After: an extended version, with the picture held

```html
<video id="brew-video" controls preload="metadata"
  src="brew-talk.webm" poster="brew-talk.jpg"
  data-described="brew-talk.extended.webm"
  data-holds="[[14.64, 3.02], [20.1, 3.7]]">
  <track kind="captions" srclang="en" label="English"
    src="brew-talk.en.vtt" data-described="brew-talk.extended.en.vtt">
</video>
<button type="button" id="describe" aria-pressed="false">
  Audio description
</button>

<script>
  const video = document.getElementById('brew-video');
  const track = video.querySelector('track');
  const plain = { video: video.getAttribute('src'), captions: track.getAttribute('src') };
  // Where the extended version holds the picture: a time in the original, and seconds held.
  const holds = JSON.parse(video.dataset.holds);
  const toExtended = (time) => time + holds
    .filter(([at]) => at < time)
    .reduce((sum, [, held]) => sum + held, 0);
  const toOriginal = (time) => {
    let shift = 0;
    for (const [at, held] of holds) {
      if (time < at + shift) break;
      // Inside a hold, go back to where it began.
      shift += Math.min(held, time - at - shift);
    }
    return time - shift;
  };
  document.getElementById('describe').addEventListener('click', (event) => {
    const button = event.currentTarget;
    const on = button.getAttribute('aria-pressed') !== 'true';
    const { currentTime, paused, playbackRate } = video;
    const captions = track.track.mode;
    video.src = on ? video.dataset.described : plain.video;
    track.src = on ? track.dataset.described : plain.captions;
    // Carry on from the same moment, at the same speed, with the captions as they were.
    video.addEventListener('loadedmetadata', () => {
      video.currentTime = on ? toExtended(currentTime) : toOriginal(currentTime);
      video.playbackRate = playbackRate;
      track.track.mode = captions;
      if (!paused) video.play();
    }, { once: true });
    button.setAttribute('aria-pressed', String(on));
  });
</script>
```

```vtt brew-talk.extended.en.vtt
WEBVTT

00:04.200 --> 00:05.674
Narrator: Weigh it out like this.

00:05.724 --> 00:09.950
Describer: 15 grams of coffee, 250 millilitres of water.

00:10.200 --> 00:14.590
Narrator: Then let the water rest a moment.
Straight off the boil, it scorches the grounds.

00:14.990 --> 00:17.510
Describer: Cool for 30 seconds after boiling.

00:18.220 --> 00:23.070
Narrator: Pour slowly, and stop here.
Keep the stream thin, and let the grounds settle as you go.

00:23.470 --> 00:26.670
Describer: Pour in three stages. Stop at three minutes.

00:27.020 --> 00:29.020
[Kitchen timer rings]
```

1. Edit an extended version of the described video (W3C technique G8). Where a description has no gap, hold the picture on a frame that still shows what it describes, play the description over the held frame, then let the video carry on. Here the picture holds twice, just after the roaster's line over the second and third cards, for 3.02 and 3.7 seconds.
2. Give it its own captions file, with each description marked as the describer's at the time it is heard.
3. Point the Audio description button at the extended files instead of the described ones.
4. List the holds on the video, in `data-holds`, so the script can map a place in one file to the same moment in the other: the extended version is longer, so the same moment comes later in it.

This fixes the Before and passes 1.2.7. The descriptions are recorded, so they sound the same in every browser, and the file plays in any player, including on a video platform that has no audio description feature of its own.

## After: a player that pauses when a description runs over

```html
<video id="brew-video" controls preload="metadata"
  src="brew-talk.webm" poster="brew-talk.jpg">
  <track kind="captions" srclang="en" label="English"
    src="brew-talk.en.vtt">
  <track kind="descriptions" srclang="en" label="English"
    src="brew-talk.descriptions.en.vtt">
</video>
<button type="button" id="describe" aria-pressed="false">
  Audio description
</button>

<script>
  const video = document.getElementById('brew-video');
  const descriptions = video.querySelector('track[kind="descriptions"]').track;
  const button = document.getElementById('describe');
  // Hidden: its cues still arrive, but the browser neither shows nor reads them.
  descriptions.mode = 'hidden';
  let current = null;
  let holding = false;
  descriptions.addEventListener('cuechange', () => {
    if (button.getAttribute('aria-pressed') !== 'true') return;
    const cue = descriptions.activeCues[0];
    if (!cue) {
      // The cue's gap is over. If its description is still being said, hold the picture.
      if (current && !video.paused) {
        holding = true;
        video.pause();
      }
      return;
    }
    // Say the description while the video plays on through its gap.
    const words = new SpeechSynthesisUtterance(cue.text);
    words.lang = descriptions.language;
    words.onend = words.onerror = () => {
      if (current !== words) return;
      current = null;
      if (holding) video.play();
      holding = false;
    };
    current = words;
    speechSynthesis.cancel();
    speechSynthesis.speak(words);
  });
  // Pressing Play during a hold skips the rest of the description.
  video.addEventListener('play', () => {
    if (!holding) return;
    holding = false;
    current = null;
    speechSynthesis.cancel();
  });
  button.addEventListener('click', () => {
    const on = button.getAttribute('aria-pressed') !== 'true';
    button.setAttribute('aria-pressed', String(on));
    if (!on) speechSynthesis.cancel();
  });
</script>
```

```vtt brew-talk.descriptions.en.vtt
WEBVTT

00:05.724 --> 00:09.950
15 grams of coffee, 250 millilitres of water.

00:14.640 --> 00:14.950
Cool for 30 seconds after boiling.

00:20.100 --> 00:20.250
Pour in three stages. Stop at three minutes.
```

1. Write each description as a cue in a `<track kind="descriptions">` file. Start it in the gap after the narration, and end it where the gap ends, or where what it describes leaves the screen: here the first cue has about four seconds, the other two a fraction of a second.
2. Set the track's mode to `hidden`. The browser then fires `cuechange` as each cue starts and ends, but doesn't draw it or read it.
3. When a cue starts and the Audio description button is pressed, say the cue's text with the Web Speech API's `speechSynthesis`, in the track's language, while the video plays on.
4. When the cue ends and the description is still being said, pause the video: the picture holds until the speech ends, then plays again. A description that fits its gap never stops the video.
5. If the viewer presses Play during a hold, stop the speech and let them go on.

This fixes the Before and passes 1.2.7 in browsers that have a speech voice for the language, with one file and one script, and the descriptions are easy to edit as text. Accessible players do this too, though they pause at every description, not only where one runs past its gap: Able Player, whose text-based descriptions pause the video when its pause option is on, and Kaltura's extended audio description plugin. On a device with no voice for the language, the browser may fail to say the description and carry straight on, or read it in a voice for another language. The descriptions are in no captions file, so someone reading the captions doesn't get them, and the voice is the device's synthetic one.

## Which option to use

Both pass 1.2.7. Use the extended version: it works in every browser and on every platform the file can be uploaded to, the descriptions are a describer's recorded voice, and they appear in its captions. The player that pauses is partial, because whether the description is said at all depends on the viewer's device; add it when the descriptions change often, or as a lighter option beside the extended version, but don't rely on it alone.

## Implementation decisions

Where the video plays decides how the extended version reaches viewers:

- Your own `<video>`: the extended file and the button, as above, or the button that reads text descriptions and pauses when one runs over. Keep the button next to the captions control, so it is as easy to reach as volume, as in [video players with no captions button in the control bar](/learn/caption-controls).
- A video platform such as YouTube or Vimeo: a described audio track, where the platform takes one, plays at the video's own pace and can't hold the picture. Upload the extended version as its own video, and link to it right beside the embed, named for what it is ("With extended audio description").
- An accessible player: Able Player reads a `kind="descriptions"` track aloud, can pause the video while it does (`data-desc-pause-default="on"`, or the viewer's own preference), and puts the text in a live region where speech synthesis isn't available. Kaltura's player has a plugin that pauses at each cue of a WebVTT file and plays the description as narration, on-screen text or both. Check what the player you ship really does before counting on it.

Hold only where the narration leaves no gap. A description that fits in a pause goes in the pause, as the first card's does here, and the video doesn't stop for it: every hold costs the viewer time, and 1.2.7's Understanding page says to use extended description only where the sense of the video would be lost without it. Hold on a frame that still shows what is described, and start the description just after the hold begins, so it doesn't run into the next line.

The extended version is longer than the original, so a viewer who switches halfway needs to land on the same moment, not the same second: the script maps the time across the holds. If the file was re-edited in other ways too, store a full time map, or start it from the beginning. Its captions file has to carry the descriptions and the new timings, since everything after the first hold moves.

With the player that pauses, the speech synthesis voice and a screen reader are two separate voices: if the viewer moves through the page during a description, both speak at once, and the player can't tell when the screen reader is talking.

When the pauses are long enough, as for the first card here, an ordinary described version is enough, as in [videos without audio description of what they show](/learn/audio-description).

## Verify the fix

1. Cover the screen and play the extended version through, or turn on the player that reads descriptions and pauses. Write down every amount, time and instruction you hear, then compare the list with the cards. Each description is said in full.
2. Watch each hold. It starts while the card it describes is still on screen, lasts until the description ends, and the video carries on by itself. Press Play during a description, and Pause just before one, and check the player does what you asked.
3. Using only the keyboard, and then a screen reader with the screen covered, find the Audio description button beside the player, turn it on halfway through, and check that the screen reader says it is pressed and that the video carries on from the same moment.
4. Turn captions on in the extended version. They match what is heard, with each description marked as the describer's, and they stay on after switching.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat whenever the video, its narration or its descriptions are replaced.

## Limits

This is a starting pattern, not a guarantee for every application. Whether a video needs extended description, and what each description should say, depends on the video: this guide can't judge that for you. Extended audio description is Level AAA, so most laws and policies that cite WCAG at Level AA don't require it; it is still the only way some videos make sense without the picture. Test the complete journey with people who will listen to it, in the players and platforms the video really plays in.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
