Audio and Video Elements
Native audio/video — controls, sources, captions, autoplay policies, and accessibility requirements for media on the web.
- 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:
- Play/pause button with correct accessible name and state
- Keyboard support (Space/Enter where expected)
- Volume and mute
- Seek slider with SR-friendly values
- Captions toggle
- 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
- Autoplay with audio.
- Missing captions on talking-head content.
- Giant
preload="auto"on landing pages. - Relying on hover-only custom controls on mobile.
- 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.
Related
- Browser screen recorder — capture screen + mic in the browser and download an H.264 MP4
- Images alt and decorative
- prefers-reduced-motion
- iframe sandbox and security
- Object-fit and aspect-ratio
Further reading
Related guides
- Accessibility Tree OverviewHow browsers build the accessibility tree from DOM and CSS — roles, names, states, what’s pruned, and how to inspect it in DevTools.
- Autocomplete and Name Attributesname and autocomplete on form fields — password managers, autofill tokens, and why missing names break real users more than demos.
- Base Element and Relative URLsHow <base href> rewrites relative URLs for links, scripts, and forms — powerful for static hosts, dangerous when set accidentally.
- contenteditable Basicscontenteditable surfaces — what the browser gives you, sanitization, keyboard and a11y gaps, and when to pick a real editor library.
- data Attributesdata-* attributes for embedding element metadata — dataset API, CSS hooks, validation limits, and when not to store app state in the DOM.