Video background for a website, done right

How to add a video background to a website that autoplays on iPhone, loads fast and keeps text readable: encoding, markup and real file budgets.

A thick slab of clear glass on pure black with a strip of amber light passing through it, like a projector beam frozen inside the glass.
Five attributes, one silent file, and a poster for everyone the file never reaches.

A website video background that works everywhere is a silent H.264 MP4 of 6 to 10 seconds, about 2.5 MB at 1920 pixels wide, with a portrait cut for phones, a poster cut from its first frame, and four attributes on the tag: autoplay, muted, loop and playsinline. The rest is the reasoning behind those numbers, and the details that quietly break them.

Video is the most convincing background a hero can have, because it can show a real place, a product in light or a character, and phones decode it in hardware. It is also easy to get wrong, because every failure is silent. A video that will not autoplay on an iPhone shows a still frame or nothing, and a 12 MB file shows nothing for the first seconds on a slow connection.

The markup that autoplays on every device#

html
<video class="hero-video" autoplay muted loop playsinline
       preload="auto" poster="/media/hero-poster.jpg" aria-hidden="true">
  <source src="/media/hero-720x1280.mp4" type="video/mp4"
          media="(max-width: 767px) and (orientation: portrait)">
  <source src="/media/hero-1920-av1.mp4" type='video/mp4; codecs="av01.0.08M.08"'>
  <source src="/media/hero-1920.mp4" type="video/mp4">
</video>

Each attribute has a documented reason. WebKit's video policy for iOS lets a video autoplay without a tap when it is muted or its file has no audio track, plays it inline on iPhone only with playsinline, and pauses an autoplaying video once it scrolls out of view. Chrome's autoplay policy states plainly that muted autoplay is always allowed.

The muted attribute has a known trap in React. Issue #10389, open since 2017, records that React sets the muted property without guaranteeing the attribute in the HTML, and the attribute is all the browser sees before any script runs. Remove the audio track from the file with -an at export, and autoplay no longer depends on the framework at all.

Source order does the codec negotiation. The browser plays the first <source> it can, so AV1 goes before H.264, and its type carries the codecs parameter; without it, every browser that plays MP4 would pick the AV1 file and stall on it. The media attribute on the phone source works in current engines again: Firefox restored it in version 120, Chrome followed, and Safari never removed it, as Scott Jehl documents. The choice is made once, when the video loads, so a rotated phone keeps its file.

What the footage should do#

The footage decides more than the code. Films that work behind a headline share three properties: one subject or one material, a camera that holds still or moves slowly, and a calm area where the type can sit.

Lacca runs the film as the whole hero and sets its headline low on the left, away from the hands. Strix uses a head-turn, the kind of shot where the subject moves and the frame does not. Both are the opposite of stock drone footage, where the whole frame changes every second and the encoder has to spend bits on all of it.

That difference shows up directly in file size. The ribbon film behind Brisk, a slow abstract loop, is 7.5 seconds at 1920 by 1080 and about 2.1 MB in H.264. The sunflower run behind Zoomies is 24 seconds of tracking shot at 1600 by 900 and about 11.7 MB. Length and camera movement set the budget before any encoder flag does.

Cut the loop so its last frame leads back into its first. If the shot cannot be matched, a crossfade of about 12 frames at 24 fps, done in the edit, hides the join better than any player setting.

Encoding settings that keep the file small#

Encode from the best master you have, and never upload the export from an editing tool unchanged. These four FFmpeg commands produce the files the markup above expects, in order: the desktop H.264 file that plays everywhere, the smaller AV1 file, the phone cut as a centered 9:16 crop, and the poster.

bash
ffmpeg -i master.mov -an -vf "scale=1920:-2:flags=lanczos,fps=24" \
  -c:v libx264 -preset slow -crf 24 -maxrate 3M -bufsize 6M \
  -profile:v high -pix_fmt yuv420p -g 48 -movflags +faststart hero-1920.mp4

ffmpeg -i master.mov -an -vf "scale=1920:-2:flags=lanczos,fps=24" \
  -c:v libsvtav1 -preset 6 -crf 35 -g 48 -pix_fmt yuv420p \
  -movflags +faststart hero-1920-av1.mp4

ffmpeg -i master.mov -an -vf "crop=ih*9/16:ih,scale=720:1280:flags=lanczos,fps=24" \
  -c:v libx264 -preset slow -crf 26 -maxrate 1.2M -bufsize 2.4M \
  -profile:v high -pix_fmt yuv420p -g 48 -movflags +faststart hero-720x1280.mp4

ffmpeg -i hero-1920.mp4 -frames:v 1 -q:v 3 hero-poster.jpg

The phone crop takes the middle of the frame. If the subject sits off center, replace crop=ih*9/16:ih with an explicit x offset, and crop from a 4K master when you have one so the 720 by 1280 file is a downscale.

AV1 support is the reason the H.264 file stays. Chrome has decoded AV1 since version 70 and Firefox since 67, per caniuse, while Safari 17 added it only on devices with an AV1 hardware decoder, starting with iPhone 15 Pro. Every other Apple device falls through to H.264, which is exactly what the source order is for.

What each flag does, and why keyframes matter#

SettingValueWhy
-crf for libx26422 to 26constant quality; the default is 23 on a 0 to 51 scale, higher is smaller
-crf for SVT-AV132 to 38its scale runs 0 to 63, default 35
-maxrate and -bufsize3M and 6Mcaps peaks so one busy second cannot break the budget
-g48 at 24 fpsa keyframe every two seconds
-pix_fmtyuv420pthe chroma format every hardware decoder plays
-anremoves the audio track
-movflags +faststartputs the index first so playback starts before the download ends

Faststart matters more than it looks. An MP4 written normally keeps its index, the moov atom, at the end of the file, and a player that has not reached it cannot begin. FFmpeg's muxer documentation describes +faststart as a second pass that moves that index to the start.

Keyframes barely matter for a plain loop, which always restarts on frame 0. They matter a great deal for video scrubbed by scroll, because every seek decodes forward from the previous keyframe, and libx264's default interval is 250 frames. A scrubbed film wants a keyframe every few frames, or on every frame at a much larger size, which is why many scroll-driven pages use an image sequence instead; the trade-off is covered in the scrollytelling guide.

HEVC is optional for an opaque loop. Safari plays it and other browsers only partly, per caniuse. If you add one, write it with -tag:v hvc1: FFmpeg tags HEVC in MP4 as hev1 by default, and Apple's players expect hvc1.

File-size budgets, and why phones need their own cut#

FileTarget sizeAt 8 seconds
Desktop H.264, 1920 by 1080up to about 2.5 MBabout 2.5 Mbps
Desktop AV1, 1920 by 1080smaller at equal qualitylower
Phone H.264, 720 by 1280up to about 1 MBabout 1 Mbps
Poster, JPEG or WebP60 to 120 KB

The phone row is the easiest one to skip and the costliest. On a 390 by 844 screen, a 16:9 film with object-fit: cover is scaled to 844 pixels tall and about 1,500 wide, so the visitor sees the middle 26% of its width while downloading all of it. A portrait cut framed around the subject shows the whole subject at well under half the bytes.

When a separate cut is impossible, object-position is the fallback. A subject at 62% across the frame stays visible on a phone with object-position: 62% 50%, and a brief should name that value rather than leave it at the default center.

The poster carries more weight than its size suggests. For a <video>, Largest Contentful Paint counts the poster's load time or the first frame's presentation, whichever comes first, and a good LCP is 2.5 seconds or less. Cut the poster from the first frame so the swap from still to moving is invisible, and preload it with fetchpriority="high".

Text over moving footage#

WCAG 1.4.3 asks for 4.5:1 for body text and 3:1 for large text, which it defines as 18 point (about 24 px) or 14 point bold (about 18.5 px). Over video, the background is every frame, so measure against the brightest area the type ever covers.

Brightest area behind white typeWhite aloneBlack scrim for 3:1For 4.5:1For 7:1
White studio #FFFFFF1:142%54%66%
Overcast sky #E6E6E61.25:136%49%62%
Pale pink #F2C4CE1.55:129%43%58%
Clear sky #87CEEB1.74:125%40%55%

A flat scrim at those values dulls the whole picture to protect one band of type. A gradient that reaches the needed opacity only behind the text keeps the rest of the film bright, and the hero section values use linear-gradient(to top, rgba(14, 16, 20, 0.72) 0%, rgba(14, 16, 20, 0) 62%) for exactly that reason.

Dark footage is the easy case. When the brightest area behind the type is already deep, light type clears the ratio with little or no scrim. Embercall sets four lines of white type over a night scene, which is that easy case chosen on purpose. Choose the footage with the type already in mind.

Transparent video in front of the headline#

Some heroes put the subject in front of the type, which takes a video with an alpha channel. Kairosveld does this: a woman stands in front of her own headline and turns her head, and the words read as being behind her.

Alpha video splits by browser. In Jake Archibald's 2024 comparison, Safari plays HEVC with alpha, Chrome and Firefox play VP9 with alpha in WebM, Safari ignores VP9's alpha, and AV1 video carries no alpha at all. So the HEVC source goes first and the WebM second. His test clip came to about 3.4 MB as HEVC with alpha and 1.1 MB as VP9.

He also describes a third route: one video twice as tall, color in the top half and alpha as grayscale in the bottom, recombined by a small WebGL shader. Encoded as AV1, that version of his clip came to about 460 KB, with a stacked HEVC file of about 1.14 MB for Apple devices that lack an AV1 decoder.

Accessibility and the pause control#

WCAG's Pause, Stop, Hide criterion, Level A, requires a way to pause moving content that starts on its own, lasts more than five seconds and sits alongside other content. A looping hero video meets all three. Reduced motion is a separate preference and should stop the video before it starts.

js
const video = document.querySelector(".hero-video");
const toggle = document.querySelector(".hero-pause");

if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
  video.pause();
  video.removeAttribute("autoplay");
  toggle.setAttribute("aria-pressed", "true");
} else {
  video.play().catch(() => video.classList.add("is-still")); // blocked: poster stays
}

toggle.addEventListener("click", () => {
  if (video.paused) video.play(); else video.pause();
  toggle.setAttribute("aria-pressed", String(video.paused));
});

The catch matters on iPhone. Low Power Mode has been reported to block autoplay even for muted video (WebKit bug 216887), and the rejected play() promise is your signal to keep the poster and hide the toggle. Give the button an accessible name, for example aria-label="Pause background video", a hit area of at least 40px, and keep the <video> itself aria-hidden="true", since it carries no information.

Briefing an AI model for a video hero#

Claude, Cursor, Lovable and Bolt all wire a video background in a few lines. What they cannot guess is the values, so the brief has to name them:

  • the source URL, autoplay muted loop playsinline, and the poster path
  • object-fit: cover and an explicit object-position
  • the phone cut and the breakpoint that selects it
  • the scrim as a gradient with stops and opacities
  • the pause toggle, its position, size and label
  • the reduced-motion behavior: poster only
  • the type sizes, so contrast is measured against the right threshold
text
Hero: full-viewport <video> behind the content, autoplay muted loop playsinline,
src = <clip URL>, poster = <poster URL>, object-fit: cover, object-position: 62% 50%.
Headline 72px/0.95, white, bottom-left, max 14ch.
Scrim under the headline only: linear-gradient(to top, rgba(10,12,16,0.7) 0%, rgba(10,12,16,0) 60%).
Pause toggle 40px, bottom-right, 24px inset, aria-label "Pause background video".
prefers-reduced-motion: show the poster, do not render the video.

The same brief works in Claude Code and in Lovable. Paste a clip URL from Thanor's animated backgrounds where it says <clip URL>.

Mistakes that break video backgrounds#

  • An audio track left in the file. It makes autoplay depend on an attribute your framework may not render.
  • One file for every screen. A 4K master served to phones costs seconds and shows a quarter of the picture.
  • No poster, or a black one. Blocked autoplay, slow networks and reduced motion all show the poster, so it has to be a real frame.
  • preload="none" on the hero. It delays the first frame of the one video that is supposed to be seen first. Keep none for videos further down.
  • A GIF instead of video. WebKit's own post presents <video autoplay loop muted playsinline> as the replacement for GIF.
  • A missing object-fit: cover. The default letterboxes the film inside the hero.
  • More motion on top. Animated type or a parallax layer over a moving film leaves the eye with nowhere to rest. For when to use video at all versus a gradient or shader, see animated website backgrounds and animated gradients.

Start from a clip already made for a hero#

Thanor's animated backgrounds page collects looping videos made for the first screen of a site, covering landscapes, abstract light, characters and objects. Copy a clip's URL into Claude, Cursor, Lovable or Bolt as the hero background, or download the file and host it yourself. The clips without a crown are free with an account, and the crowned ones come with a paid plan.

Every clip there is an H.264 MP4 with no audio track, so the audio half of the iPhone autoplay rule is already satisfied. What a library cannot know is your layout, so the phone cut and the poster are still yours to make, and the commands above make both.

Download one clip from the backgrounds page, run the phone and poster commands on it, and paste the brief above with your URLs. That is a finished, measured video hero, and the same steps apply to any footage you shoot yourself.

Questions this raises

Why won't my background video autoplay on iPhone?

iOS autoplays a video without a tap only when it is muted or its file has no audio track, and plays it inline only with the playsinline attribute. Check that the rendered HTML really contains muted, since React does not guarantee that attribute, or remove the audio track from the file. Low Power Mode has also been reported to block autoplay, so always set a poster image.

What is the best video format for a website background?

An MP4 with H.264 video, yuv420p color, no audio track and the faststart flag, which every browser plays. Add an AV1 MP4 as the first source with codecs="av01.0.08M.08" in its type for browsers that decode AV1, and keep H.264 as the fallback. Use HEVC with alpha only when you need transparency in Safari.

How big should a background video file be?

Aim for about 2.5 MB for a 1920 pixel wide desktop loop of 6 to 10 seconds, which is roughly 2 to 2.5 Mbps, and about 1 MB for a 720 by 1280 phone cut. The poster image should be 60 to 120 KB. Shortening the loop and holding the camera still save more bytes than lowering the resolution.

Does a video background slow down my website?

It can delay the first paint if the file is large and has no poster. For a video, Largest Contentful Paint uses the poster's load time or the first frame, whichever is earlier, so a small poster keeps the score healthy. Serve a separate phone cut, add faststart so playback begins early, and pause or skip the video under reduced motion.

Does a background video need a pause button?

Under WCAG 2.2 success criterion 2.2.2, Level A, moving content that starts automatically, lasts more than five seconds and is shown alongside other content needs a way to pause, stop or hide it. A looping hero video meets those conditions, so add a visible pause toggle with an accessible name and keep its state in aria-pressed.

Part of

Inspiration, with the prompt

Examples of the looks and effects people search for, from 3D scenes to scroll animation, each one shown working and paired with the specification that builds it.