
Last Update: February 20, 2025 3 min read
BY
eric
Video Not Autoplaying in React or Next.js? Use autoPlay, Not autoplay
Short answer: in JSX the attribute is autoPlay with a capital P. If you copy plain HTML and write autoplay, React doesn't recognise it, drops it, and your video loads but just sits on the first frame. Write it like this:
<video autoPlay muted loop playsInline>
<source src="/videos/clip.mp4" type="video/mp4" />
</video>
muted and playsInline aren't optional extras. Browsers block autoplay with sound, and iPhones won't play a video inline without playsinline.
What happened
I made a short clip with Sora and tried to use it in a Next.js page. The video loaded, but it never started playing. This is the code I had (with a public Wikimedia test clip instead of my Sora video):
export default function Page() {
return (
<div>
<h1>Video</h1>
<video autoplay muted loop>
{/* This is not the video I created with Sora */}
<source src="https://upload.wikimedia.org/wikipedia/commons/0/09/Frozen_drop.webm" type="video/webm" />
Your browser does not support the video tag.
</video>
</div>
);
}
It looks right because it's valid HTML. In React it isn't, and the fix was changing autoplay to autoPlay.
Why lowercase autoplay is ignored
React uses camelCase names for DOM attributes: className, tabIndex, autoPlay, playsInline. A lowercase autoplay is treated as an unknown attribute. Writing it with no value means autoplay={true}, and React won't put a boolean true on an attribute it doesn't know. So it's left out of the HTML.
You can see this in the browser console in development:
Invalid DOM property `autoplay`. Did you mean `autoPlay`?
Here is what React 19 actually renders for each spelling. It is the same from server rendering and in the browser:
An earlier version of this post suggested autoPlay="true", autoPlay="autoplay" or autoPlay="". The first two work but trigger a warning, and autoPlay="" doesn't work at all because React treats an empty string as false. Just write autoPlay on its own, or autoPlay={true}.
The same applies to the other video attributes. Use playsInline, not playsinline. muted, loop, controls and poster are already single words, so they're fine as they are.
The fixed component
export default function Page() {
return (
<div>
<h1>Video</h1>
<video autoPlay muted loop playsInline>
<source src="https://upload.wikimedia.org/wikipedia/commons/0/09/Frozen_drop.webm" type="video/webm" />
Your browser does not support the video tag.
</video>
</div>
);
}
This works as a Server Component in the Next.js App Router. It doesn't need "use client" because there's no state or effect.
If it still won't autoplay
Getting the attribute name right is the usual cause. If the video still sits there, check these:
-
Is it muted? Chrome, Safari and Firefox all block autoplay with sound unless the user has already interacted with the site.
mutedis the reliable way through. If you need sound, start muted and add an unmute button. -
Is
playsInlinethere? On iPhone, a video withoutplaysinlinewon't play inline on the page. AddplaysInlinefor any background or hero video. -
Can the browser play the format?
.webmisn't supported everywhere, particularly on older Apple devices. H.264 MP4 is the safe default. You can list both and the browser picks the first one it supports:jsx<video autoPlay muted loop playsInline> <source src="/videos/clip.webm" type="video/webm" /> <source src="/videos/clip.mp4" type="video/mp4" /> </video>If you need to convert or shrink a clip, my FFmpeg on Ubuntu commands post has the commands.
-
Is the device in a power-saving mode? Some phones in low power or data-saver modes don't autoplay video at all. Your code can't override that, so set a
posterimage so the page still looks right. -
Is the file path right? In Next.js, files in
public/are served from the site root.public/videos/clip.mp4is/videos/clip.mp4, not/public/videos/clip.mp4. Check the Network tab for a 404.
Fallback: start it with play() and catch the error
If you want to know when autoplay was blocked, or you swap the src after the first render, start the video from an effect. play() returns a promise that rejects (usually with NotAllowedError) when the browser refuses. In the App Router this has to be a Client Component:
"use client";
import { useEffect, useRef } from "react";
export default function BackgroundVideo({ src, poster }) {
const ref = useRef(null);
useEffect(() => {
const video = ref.current;
if (!video) return;
video.muted = true; // set the property as well, before calling play()
video.play().catch((err) => {
// Autoplay was blocked. The poster stays visible.
console.warn("Autoplay blocked:", err.name);
});
}, [src]);
return (
<video ref={ref} src={src} poster={poster} autoPlay muted loop playsInline />
);
}
Setting video.muted = true in code is belt and braces. React sets muted as a DOM property rather than an attribute when it renders in the browser, so you won't see muted when you inspect the element even though the video is muted.
The video
Here's the Sora clip that started all this:
If you're comparing AI video tools, see Exploring AI video generation.









Comments (0)
Leave a Comment