Audio and video

Embed video and audio that loads sensibly, plays where it is allowed to, and has captions.

09of 22
≈ 11Min read
8Parts

video and audio are the same element with a picture

Both take a src, or several <source> children so the browser can pick a format it supports. Both take controls, autoplay, loop, muted and preload. Everything you learn about one applies to the other.

a video that behaves
<video src="/media/showroom.mp4"
       poster="/media/showroom-poster.jpg"
       width="1600" height="900"
       controls preload="metadata" playsinline>
  <track kind="captions" src="/media/showroom.en.vtt" srclang="en" label="English" default>
  Your browser cannot play this video.
  <a href="/media/showroom.mp4">Download it instead</a>.
</video>

The text inside the element is the fallback: it is shown only when the browser cannot play video at all.

The attributes that matter

AttributeWhat it does
controlsShows the browser's own player. Without it there is no way to play unless you build one.
posterA still shown before playback. Without it you get a black rectangle or the first frame.
preload="none"Load nothing until the reader presses play. Best for a video most people will skip.
preload="metadata"Load only the duration and dimensions. A good default.
preload="auto"A hint to buffer. Use only when playback is the point of the page.
playsinlinePlays in place on iOS instead of taking over the screen. Always include it.
mutedStarts silent. Required for autoplay to be allowed.
loopRepeats forever.
width / heightReserve the space, exactly as with an image.

Several formats

Sources are tried in order and the first playable one wins. Put the smallest modern format first.

MP4 with a modern alternative
<video controls playsinline poster="/media/poster.jpg" width="1280" height="720">
  <source src="/media/clip.webm" type="video/webm">
  <source src="/media/clip.mp4" type="video/mp4">
</video>
H.264 in an MP4 still plays everywhere

If you ship only one file, ship that one. WebM and AV1 are smaller, but they are an optimisation on top, not a replacement.

Autoplay is not yours to decide

Browsers block autoplay with sound. A video may start on its own only if it is muted — and even then some settings and low-power modes will refuse.

So: never rely on autoplay for anything the reader needs, always provide controls or your own play button, and treat play() as something that can be rejected.

play() returns a promise
const v = document.querySelector("video");

v.play().catch(() => {
  // blocked: show a play button instead of failing silently
  document.querySelector(".play-fallback").hidden = false;
});

A muted, looping, playsinline video with no controls is the standard way to use video as decoration — and it should carry aria-hidden="true" if it says nothing.

Captions and tracks

A <track> adds a caption or subtitle file in WebVTT format. Captions are not optional extras: they serve deaf readers, anyone in a noisy place, and anyone watching without sound — which on social platforms is most people.

  • kind="captions" — dialogue plus important sound, in the same language.
  • kind="subtitles" — a translation. Needs srclang.
  • kind="descriptions" — what is happening on screen, for blind readers.
  • kind="chapters" — navigation points.
  • default — the track to switch on automatically.
captions.vtt
WEBVTT

00:00:00.000 --> 00:00:03.400
The door sits flush with the wall.

00:00:03.400 --> 00:00:07.900
A one millimetre gap runs all the way round it.
Tracks need an origin

A VTT file loaded from a different domain needs CORS headers, and file:// will not load one at all. Test on a local server.

Audio

<audio> with controls is a complete player. Give it a caption saying what it is: the control bar alone tells a screen reader nothing about the content.

a track from the archive
<figure>
  <figcaption>First Impulse — deep house, 2:34</figcaption>
  <audio src="/audio/first-impulse.mp3" controls preload="none"></audio>
</figure>

Controlling playback yourself

Seeking needs the server to support HTTP range requests. Without them currentTime appears to work and the picture never changes — a confusing failure worth recognising.

the API is small
const v = document.querySelector("video");

v.currentTime = 12.5;     // seek, in seconds
v.playbackRate = 1.5;
v.volume = 0.4;           // 0 to 1
v.muted = true;

v.addEventListener("loadedmetadata", () => console.log(v.duration));
v.addEventListener("timeupdate", () => console.log(v.currentTime));
v.addEventListener("ended", () => console.log("finished"));

Weight

  • Video is the heaviest thing on most pages. A ten-second clip can outweigh every other asset together.
  • Use preload="none" and a poster for anything not central.
  • For a decorative loop, cut the resolution: it is scaled down anyway.
  • A short muted video is far smaller than an animated GIF of the same thing. Never ship a GIF.

Practice