ESC

Type to search the knowledge base.

Audio and Video Elements

Native audio/video — controls, sources, captions, autoplay policies, and accessibility requirements for media on the web.

intermediate3 min read
  • html
  • audio
  • video
  • media

<audio> and <video> give you playback, buffering, and fullscreen hooks without a proprietary plugin. Most product pain is autoplay policy, captions, and custom controls that break keyboard access.

Docs: MDN <video>, <audio>, Media accessibility.

Basic markup

<video
  controls
  playsinline
  preload="metadata"
  poster="/talk-poster.jpg"
  width="1280"
  height="720"
>
  <source src="/talk.webm" type="video/webm" />
  <source src="/talk.mp4" type="video/mp4" />
  <track
    kind="captions"
    srclang="en"
    src="/talk-en.vtt"
    label="English"
    default
  />
  Download the <a href="/talk.mp4">MP4</a>.
</video>
<audio controls preload="none">
  <source src="/episode.mp3" type="audio/mpeg" />
</audio>
  • controls — native UI (accessible baseline).
  • playsinline — critical on iOS so videos don’t force fullscreen.
  • preload — none | metadata | auto (bandwidth budget).
  • Fallback text inside the element for ancient clients / download links.

Multiple sources

Browsers pick the first supported <source>. Order modern efficient formats first when appropriate (WebM/AV1) with MP4 H.264 fallback for reach. Don’t list a type the file isn’t.

Captions and subtitles

<track kind="captions" srclang="en" src="/en.vtt" label="English" default />
<track kind="subtitles" srclang="es" src="/es.vtt" label="Español" />

WebVTT files provide timed cues. Captions are not optional for spoken content in accessible products — WCAG media requirements apply. Custom players must still expose a captions toggle.

Autoplay reality

<video autoplay muted loop playsinline poster="/bg.jpg">
  <source src="/loop.mp4" type="video/mp4" />
</video>

Autoplay with sound is widely blocked. Muted autoplay is often allowed. Always provide a pause control for motion (vestibular / distraction). Prefer prefers-reduced-motion to disable decorative loops:

const video = document.querySelector("video.decor");
if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
  video.removeAttribute("autoplay");
  video.pause();
}

Custom controls checklist

If you hide native controls:

  1. Play/pause button with correct accessible name and state
  2. Keyboard support (Space/Enter where expected)
  3. Volume and mute
  4. Seek slider with SR-friendly values
  5. Captions toggle
  6. Don’t trap focus oddly in fullscreen

Shipping a div overlay without keyboard support is a regression from native.

Picture-in-picture / fullscreen

await video.requestFullscreen();
await video.requestPictureInPicture();

Feature-detect; handle rejections. Fullscreen isn’t required for basic a11y but layout of custom chrome must remain usable.

Interview out-loud

“I use native audio/video with controls as the baseline, multiple sources for format fallback, and WebVTT tracks for captions. Autoplay needs muted + playsinline and should yield to reduced motion. Custom players must reimplement keyboard, names, and captions — not just skins.”

Footguns

  1. Autoplay with audio.
  2. Missing captions on talking-head content.
  3. Giant preload="auto" on landing pages.
  4. Relying on hover-only custom controls on mobile.
  5. No transcript alternative for audio-only podcasts when required.

Transcripts and descriptions

For prerecorded video with speech, provide captions. For audio-only podcasts, provide a transcript page or expandable transcript. Descriptive audio for critical visual information is a higher bar for some content types — know your WCAG target.

<video controls>
  <source src="/demo.mp4" type="video/mp4" />
  <track kind="captions" src="/demo.vtt" srclang="en" label="English" default />
  <track kind="descriptions" src="/demo-desc.vtt" srclang="en" label="English descriptions" />
</video>
<details>
  <summary>Transcript</summary>
  <p>…</p>
</details>

Codec reality

MP4/H.264 remains the compatibility baseline. WebM/AV1 can save bandwidth where supported — offer multiple <source>s. Test iOS Safari specifically for playsinline, autoplay, and fullscreen behaviors.

Live regions for custom players

If you replace native controls, announce state changes:

<div id="player-status" class="sr-only" aria-live="polite"></div>
status.textContent = playing ? "Playing" : "Paused";

Do not spam the live region on every timeupdate tick — only on play/pause/ended/error. Volume sliders need accessible names and keyboard adjustments.

Further reading

Related guides