·

·

10 min read

Why your homepage video won’t autoplay

Every major browser allows muted autoplay, yet hero videos still sit frozen on their poster. Every cause we found, in the order to check, with the fix for each.

PostHog example film: “PostHog already has your analytics and errors” over six product screens

The rule is simple, and every major browser follows it: a muted video may play by itself. Chrome’s policy starts with the sentence “Muted autoplay is always allowed.” [1] Safari has let muted video autoplay on iPhone since 2016, and Firefox kept muted autoplay allowed when it began blocking audible autoplay in 2019. [2] [3]

And yet a lot of homepage videos sit frozen on their poster. When that happens, the cause is almost never a mysterious bug. It’s one of about a dozen ordinary ones, and several of them only show up for visitors, never on the developer’s own machine. We went through them one by one, checked each against the browsers’ documentation and source code, and tested the ones we could in current builds of Chrome’s and Safari’s engines. Here they are, in the order worth checking.

Start from markup that works

<video autoplay muted loop playsinline
       poster="/video/hero.jpg"
       width="1920" height="1080">
  <source src="/video/hero.mp4" type="video/mp4">
</video>
<video autoplay muted loop playsinline
       poster="/video/hero.jpg"
       width="1920" height="1080">
  <source src="/video/hero.mp4" type="video/mp4">
</video>
<video autoplay muted loop playsinline
       poster="/video/hero.jpg"
       width="1920" height="1080">
  <source src="/video/hero.mp4" type="video/mp4">
</video>

When a video won’t play, compare this with what the browser actually has, not with your source code. Open the inspector and look at the live element. Several of the causes below are cases where the two differ. (How to put a video on your homepage without slowing it down explains each attribute and the file settings.)

What you see tells you where to look

  • The poster, with Safari’s own play button on top. Safari decided not to start it, usually because of Low Power Mode or a setting. Jump to 9.

  • The poster or first frame, no button, nothing moving. Autoplay was refused or never began. Start at 1.

  • A black or empty box. Usually the file or the server. Start at 6.

1. Chrome doesn’t think it’s muted

muted is what makes autoplay allowed, so it’s the first thing to check. Three traps catch people here.

The file has no sound, so you left muted off. Safari and Firefox treat a file with no audio track as silent and let it autoplay. [2] [4] Chrome doesn’t. Its code decides eligibility from whether the element is muted, not from whether the file has audio, and in our tests a video with no audio track and no muted attribute stayed frozen in Chrome while it played in Safari’s engine. [5] MDN’s autoplay guide says the opposite, so this one catches careful people too. Always add muted, even to a silent file.

Your script set the attribute, not the property. In Chrome, the muted attribute only takes effect when the HTML parser sees it. If you build the element in JavaScript and call setAttribute("muted", ""), the video isn’t muted and won’t autoplay. Set the property instead: video.muted = true. [5]

Something copied the element. React sets muted as a property when it renders on the client, so the element plays but carries no muted attribute. That’s fine until another script clones it. Carousels and marquees that duplicate slides for a seamless loop do exactly that, and a clone takes its muted state from the attribute, so the copy is audible and stays frozen. [6] Setting video.defaultMuted = true in an effect writes the attribute back.

2. There’s no playsinline

On iPhone, a video without playsinline is only allowed to play full screen. WebKit’s policy says such elements “will continue to require fullscreen mode for playback on iPhone.” [2] Going full screen needs a tap, so an autoplaying hero without the attribute just sits on its poster.

There’s one version of this you can’t fix. Apps that open links in their own built-in browser on iPhone may not allow inline video at all: Apple’s setting for it defaults to off on iPhone, and when it’s off, even playsinline doesn’t help. [7] Which apps leave it off isn’t documented. Plan for the poster.

3. play() failed and nothing noticed

If your code calls video.play(), the browser answers with a promise, and the promise is rejected when playback isn’t allowed. [1] With nothing to catch it, the video stays frozen and the only trace is a line in the console. These are the messages to look for:

  • Chrome: “NotAllowedError: play() failed because the user didn't interact with the document first.” Chrome thinks the video is audible. Go back to 1.

  • Safari: “NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.” Safari refused it: the video is audible or not visible, the phone is in Low Power Mode, or a setting blocks it.

  • Any browser: “AbortError: The play() request was interrupted by a call to pause().” This one is your own page. Something is calling pause() or load(), or changing the source, straight after play(). [8]

4. It isn’t visible

Browsers save power by not playing video nobody can see. Chrome starts a muted autoplay video only once it’s in the viewport, and pauses it when it scrolls away. [9] [5] In our tests, a video inside a display: none parent never started, and started the moment the parent was shown. [5] Safari on iPhone is stricter: a video has to be “scrolled into the viewport, made visible through CSS, and inserted into the DOM” before it starts. [2]

The common way to hit this is two heroes, one for desktop and one for mobile, with one of them hidden by a media query. Use one element, or call play() yourself when you reveal it.

5. autoplay arrived too late

The browser checks autoplay once, when the video first has enough data to play. Add the attribute with JavaScript after that and nothing happens. [10] Lazy-loading scripts cause a related failure: swapping the src of a <source> element does nothing until you call video.load(), a rule the HTML spec states in so many words. [10] And Chrome now supports loading="lazy" on video, which is good for videos further down the page and only ever delays the one at the top. [11]

Put autoplay muted playsinline in the initial HTML of the hero, and leave lazy loading to everything below it.

6. The file can’t start

Some videos are allowed to play and can’t.

  • Use H.264 as the baseline. An H.264 MP4 plays in every current browser. HEVC depends on the device, and WebM only reached iPhones with iOS 17.4. [12] [13]

  • If you do ship HEVC, check the tag. Safari’s engine refused an HEVC MP4 tagged hev1 and played the same stream tagged hvc1. FFmpeg writes hev1 by default, so add -tag:v hvc1. [5] [14]

  • Look for a media error, not a rejection. A file the browser can’t decode doesn’t reject play() with NotAllowedError. It sets video.error instead, with code 4, which the HTML standard names MEDIA_ERR_SRC_NOT_SUPPORTED. [10]

  • Put the index at the front. An MP4’s index (the moov atom) is written at the end of the file unless you ask otherwise. Served without byte-range support, our test file with the index at the end couldn’t start until the whole file had downloaded. Encode with -movflags +faststart. [5] [15]

7. The server gets in the way

  • Byte ranges. Apple’s guidance for iPhone is that servers hosting video “must support byte-range requests”. Check with curl --range 0-99 -o /dev/null -w '%{http_code}' URL; the answer should be 206. [16]

  • The content type. Serve .mp4 as video/mp4. Current desktop browsers are forgiving, but Safari only fixed a failure caused by mismatched types in September 2026. [17]

  • Service workers. A service worker that answers a video’s range request with the whole file can break playback. Workbox needs its range-requests module for this. [18]

  • CORS. If the element has a crossorigin attribute and the file moves to a CDN that doesn’t send Access-Control-Allow-Origin, the video fails in both Chrome and Safari. [5]

8. It’s in an iframe or someone else’s player

A page builder that drops your embed into <iframe sandbox> without allow-scripts blocks autoplay completely; the sandbox’s rules block “automatically playing a video”. [19] The allow="autoplay" attribute matters mainly for sound, but add it anyway. [1]

Hosted players have their own rules. YouTube defaults to full-screen playback on iPhone unless you pass playsinline=1, and looping a single video needs playlist set to the same video ID alongside loop=1. Its documented way to mute is the player API; the common mute=1 parameter works but isn’t in YouTube’s reference. [20] Vimeo needs muted=1 with autoplay=1, or background=1 on paid plans, and on unlisted videos every extra parameter has to follow an &, not a second ?. [21]

For a homepage hero, hosting the file yourself is simpler: no player interface, no extra scripts, and your own poster.

9. The visitor has turned it off

Some refusals come from settings you can’t overrule.

  • Low Power Mode on iPhone. Safari won’t autoplay any video, muted or not, while the phone is in Low Power Mode; it shows its own play button instead. The change went into WebKit in 2017 “to save battery”, and Vimeo’s help centre says the same about its player. [22] [23] On iPhones with Adaptive Power turned on, Low Power Mode switches itself on below 20% battery, so some of your visitors are in it without having chosen it. [24]

  • Safari on the Mac has a per-site Auto-Play setting: “Allow All Auto-Play”, “Stop Media with Sound” and “Never Auto-Play”, which blocks muted video too. [25]

  • Firefox offers “Block Audio and Video” alongside its default, and Firefox for Android can block all autoplay on mobile data. [4]

  • Edge can be set to block autoplay, and uBlock Origin’s “No large media elements” switch now blocks autoplay as well. [26] [27]

10. Your own code is stopping it

If your site honours prefers-reduced-motion, the video won’t autoplay for visitors who ask their device for less motion, which is exactly what should happen. Browsers don’t stop video for them on their own; it takes your code. [28] WCAG also asks for a way to pause any motion that starts by itself and lasts more than five seconds. [29] Before blaming the browser, check whether a theme, template or library of yours has a reduced-motion branch.

The fallback is part of the design

After all of this, some visitors still won’t see the video move. The ones in Low Power Mode, the ones who switched autoplay off, the ones inside an app’s browser. For them, the poster is your hero. Choose it like a still ad: your headline over your product, never a black first frame. (The first three seconds of a product video has more on that frame.) Then show a play button only when the video isn’t actually playing:

const video = document.querySelector("video");
const button = document.querySelector(".play");
video.muted = true;
video.play()?.catch((error) => {
  if (error.name === "NotAllowedError") {
    button.hidden = false;
  }
});
button.onclick = () => {
  video.play();
  button.hidden = true;
};
const video = document.querySelector("video");
const button = document.querySelector(".play");
video.muted = true;
video.play()?.catch((error) => {
  if (error.name === "NotAllowedError") {
    button.hidden = false;
  }
});
button.onclick = () => {
  video.play();
  button.hidden = true;
};
const video = document.querySelector("video");
const button = document.querySelector(".play");
video.muted = true;
video.play()?.catch((error) => {
  if (error.name === "NotAllowedError") {
    button.hidden = false;
  }
});
button.onclick = () => {
  video.play();
  button.hidden = true;
};

The order to check things

  1. Test as a stranger. Use a private window, land on the page directly and don’t click anything. Your own clicks are why it works for you: Chrome allows more once you’ve interacted with a site. [1]

  2. Read the console. NotAllowedError means policy or power. AbortError means your code. A CORS message or a media error means the file or the server.

  3. Inspect the live element. In the console, video.muted should be true and playsinline should be present.

  4. Check visibility. No display: none parent, no hidden duplicate for another screen size.

  5. Check the file. H.264, avc1 or hvc1, faststart.

  6. Check the server. video/mp4, 206 for a range request, CORS headers if you use crossorigin.

  7. Check embeds. playsinline=1 and a mute on YouTube, muted=1 on Vimeo, no bare sandbox.

  8. Try an iPhone in Low Power Mode. What you see is your poster doing its job.

Every film we deliver is a silent, fast-start H.264 MP4 with a poster image and embed markup that follows this list. (Design your product video for sound off explains why silent is the right default in the first place.)

Notes

  1. Chrome for Developers, “Autoplay policy in Chrome”.

  2. WebKit, “New video policies for iOS” (July 2016).

  3. Mozilla Hacks, “Firefox 66 to block automatically playing audible video and audio” (February 2019): “Muted autoplay is still allowed.”

  4. Mozilla Support, “Allow or block media autoplay in Firefox”. Firefox treats a video as inaudible when it is muted, at zero volume, or has no audio track once its metadata has loaded (IsMediaElementInaudible in Firefox’s source).

  5. Our tests, September 26, 2026, in Chromium 153 and WebKit 26.6 (the engines behind Chrome and Safari, run through Playwright on macOS with a fresh profile and no user interaction). Chromium’s IsEligibleForAutoplayMuted checks the element’s effective volume and not its audio tracks (autoplay_policy.cc). MDN’s autoplay guide says autoplay blocking doesn’t apply when the source has no audio track, which matches Safari and Firefox but not Chrome.

  6. React issue #10389, open since 2017. React writes muted into server-rendered HTML (we checked versions 16 to 19) but sets only the property on the client.

  7. Apple Developer Documentation, allowsInlineMediaPlayback: “The default value of this property is false for iPhone and true for iPad.”

  8. Chrome for Developers, “DOMException: The play() request was interrupted”.

  9. Chrome Platform Status, “Pause autoplaying muted video by attribute when invisible”.

  10. WHATWG HTML Standard, media elements and the source element: “Dynamically modifying a source element’s src or type attribute when the element is already inserted in a video or audio element will have no effect.”

  11. Chrome for Developers, Chrome 148 release notes.

  12. MDN, web video codec guide.

  13. WebKit, “WebKit features in Safari 17.4”: “Now, WebM is fully supported everywhere.”

  14. Apple, HLS authoring specification, item 1.10, which prefers hvc1 to hev1.

  15. FFmpeg documentation, MOV/MP4 muxer options, faststart.

  16. Apple, Safari Web Content Guide (archived). We couldn’t reproduce a failure on desktop Safari’s engine; the requirement is documented for iPhone.

  17. WebKit, “WebKit features for Safari 27.0” (September 2026).

  18. Chrome for Developers, “Serving cached audio and video”.

  19. WHATWG HTML Standard, sandboxing: the sandboxed automatic features flag “blocks features that trigger automatically, such as automatically playing a video”, unless the sandbox allows scripts.

  20. YouTube, embedded player parameters and IFrame Player API.

  21. Vimeo Help Center, “About player parameters” and “Troubleshooting: autoplay restrictions”.

  22. WebKit bug 168985, “[iOS] Disable autoplay of silent videos in low power mode” (March 2017).

  23. Vimeo Help Center, “Troubleshooting: autoplay restrictions”: “Autoplay is disabled when the iPhone or iPad is in Low Power Mode.”

  24. Apple Support, “Use Low Power Mode to save battery life on your iPhone”.

  25. Apple, Safari User Guide, “Stop autoplay videos in Safari on Mac”.

  26. Microsoft, “How to manage autoplay in Microsoft Edge”.

  27. uBlock Origin wiki, “Per-site switches”.

  28. MDN, prefers-reduced-motion.

  29. W3C, WCAG 2.2, 2.2.2 Pause, Stop, Hide.

Get a video in 24 hours.

No meetings, no scripts, no templates.