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.
<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
| Attribute | What it does |
|---|---|
controls | Shows the browser's own player. Without it there is no way to play unless you build one. |
poster | A 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. |
playsinline | Plays in place on iOS instead of taking over the screen. Always include it. |
muted | Starts silent. Required for autoplay to be allowed. |
loop | Repeats forever. |
width / height | Reserve 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.
<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>
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.
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. Needssrclang.kind="descriptions"— what is happening on screen, for blind readers.kind="chapters"— navigation points.default— the track to switch on automatically.
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.
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.
<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.
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 aposterfor 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.