Autoplay videos are excellent for grabbing attention, but getting them to play reliably across all browsers requires the right attributes and optimization strategy.
What we'll cover:
- Why browsers block autoplay and the three attributes that fix it.
- How to reduce video file size by up to 98% using ImageKit.
- Building hero backgrounds and scroll-triggered playback controls.
You can find the complete working code in our Next.js Video Autoplay repository. All examples below are drawn directly from this project.
Understanding why autoplay fails
Modern browsers enforce strict autoplay policies. The rule to remember is videos must be muted to autoplay.
| Condition | Chrome | Safari | iOS Safari |
|---|---|---|---|
| Unmuted video | Blocked | Blocked | Blocked |
| Muted video | Works | Works | Works |
| Muted + playsInline | Works | Works | Works |
Three attributes make autoplay work everywhere:
autoPlay— tells the browser to start playing immediatelymuted— satisfies the "no surprise audio" policyplaysInline— prevents iOS from playing videos in fullscreen
Now that you understand how it works, let's build it!
Step 1: Basic AutoplayVideo component
Here's a simple component that works across all modern browsers. It includes all three required attributes (autoPlay, muted, and playsInline) so autoplay works out of the box on every platform.
// components/AutoplayVideo.tsx
'use client';
interface AutoplayVideoProps {
src: string;
poster?: string;
className?: string;
}
export function AutoplayVideo({
src,
poster,
className = '',
}: AutoplayVideoProps) {
return (
<video
src={src}
autoPlay
muted
playsInline
loop
poster={poster}
className={className}
/>
);
}
This works with any video URL. But a 60MB hero video will still kill your page load.
Step 2: Optimizing video delivery with ImageKit
You could manually encode videos with ffmpeg. Resize them. Strip audio. Convert formats. Then re-encode every time you change dimensions or quality settings.
Or you could do it with URL parameters.
ImageKit's Video API transforms videos on the fly. Upload once, then resize, compress, and convert by changing the URL.
Here's a video hosted on ImageKit:
https://ik.imagekit.io/your_imagekit_id/hero.mp4Resize and optimize
Add a width parameter to resize it:
https://ik.imagekit.io/your_imagekit_id/hero.mp4?tr=w-1280| Parameter | What it does |
|---|---|
w-1280 | Resize to 1280px width (height adjusts automatically) |
ImageKit also applies default format and quality optimizations automatically. It serves WebM to browsers that support it, like Chrome, and MP4 to others like Safari, which typically saves 30-50% on file size.
The default compression quality is set to 50% in the dashboard settings and works well for most use cases.
The difference
Just resizing and letting ImageKit handle format conversion drops the file size dramatically:
| Metric | Original | Optimized | Improvement |
|---|---|---|---|
| File Size | 63.76 MB | 0.99 MB | 98% smaller |
| Load Time | 273 ms | 34 ms | 87% faster |

Strip audio for background videos
For autoplay videos used as hero backgrounds where audio isn't needed, you can strip the audio track entirely using the ac-none parameter:
https://ik.imagekit.io/your_imagekit_id/hero.mp4?tr=w-1280,ac-none| Parameter | What it does |
|---|---|
ac-none | Strip the audio track completely |
Removing the audio codec guarantees the video is muted at the file level and shaves off additional bytes. This is especially useful for background videos and hero sections where audio would never play.
ImageKit has a free tier that covers small projects. You can connect your own S3 bucket or upload directly to the Media Library. Sign up here to try it.
The demo repository includes an OptimizationComparison component that shows real-time before/after metrics.
Setting up the SDK
Install the ImageKit Next.js SDK:
npm install @imagekit/nextCreate a providers wrapper with your ImageKit URL endpoint:
// app/providers.tsx
'use client';
import { ImageKitProvider } from '@imagekit/next';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ImageKitProvider
urlEndpoint={process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT!}
transformationPosition="query"
>
{children}
</ImageKitProvider>
);
}Wrap your layout:
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Updated AutoplayVideo with ImageKit
With the SDK configured, update the component to use ImageKit's Video component. Here's what this implementation does:
- Uses ImageKit's
Videocomponent for built-in optimization and format negotiation. - Generates a thumbnail automatically by appending
/ik-thumbnail.jpgto the video path, which extracts the first frame as a static preview image. - Combines
preload="none"with the generated poster to prevent the video from downloading until playback, saving bandwidth while still showing a preview.
// components/AutoplayVideo.tsx
'use client';
import { Video, buildSrc } from '@imagekit/next';
interface AutoplayVideoProps {
path: string;
transformation?: Array<Record<string, string | number>>;
className?: string;
}
export function AutoplayVideo({
path,
transformation = [{ width: 1280, audioCodec: 'none' }],
className = '',
}: AutoplayVideoProps) {
// Generate a thumbnail from the first frame of the video
const poster = buildSrc({
urlEndpoint: process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT!,
src: `${path}/ik-thumbnail.jpg`,
transformation: [{ width: 1280 }],
});
return (
<Video
src={path}
transformation={transformation}
autoPlay
muted
playsInline
loop
poster={poster}
preload="none"
className={className}
/>
);
}Usage:
<AutoplayVideo
path="/hero.mp4"
transformation={[{ width: 1280, audioCodec: 'none' }]}
/>You can also extract a thumbnail from a specific time point using the so (start offset) parameter. For example, ?tr=so-5 on the thumbnail URL grabs the frame at the 5-second mark. See the thumbnail docs for more options.
Step 3: Background video pattern
A background video differs from a standard autoplay component in two ways: it fills the entire viewport behind your content, and it should always have its audio stripped since users will never hear it. Here's a reusable component that wraps our AutoplayVideo with a full-screen layout and dark overlay:
// components/BackgroundVideo.tsx
'use client';
import { AutoplayVideo } from './AutoplayVideo';
interface BackgroundVideoProps {
path: string;
transformation?: Array<Record<string, string | number>>;
children: React.ReactNode;
}
export function BackgroundVideo({ path, transformation, children }: BackgroundVideoProps) {
return (
<section className="relative h-screen w-full overflow-hidden">
<div className="absolute inset-0 z-0">
<AutoplayVideo
path={path}
transformation={transformation}
className="h-full w-full object-cover"
/>
</div>
<div className="absolute inset-0 z-10 bg-black/50" />
<div className="relative z-20 h-full flex items-center justify-center">
{children}
</div>
</section>
);
}Since background videos should never have audio, use ac-none to strip the audio track:
<BackgroundVideo
path="/hero.mp4"
transformation={[{ width: 1280, raw: 'ac-none' }]}
>
<h1 className="text-5xl font-bold text-white">Your Headline</h1>
</BackgroundVideo>
Creating loops from longer videos
If your source video is 30 seconds but you only need a 5-second loop, trim it server-side instead of downloading the whole file:
<BackgroundVideo
path="/hero.mp4"
transformation={[{
width: 1280,
raw: 'ac-none,so-2,du-5',
}]}
>
<h1>Your Headline</h1>
</BackgroundVideo>The so parameter sets where the clip begins, and du controls how long it plays. This extracts a 5-second segment starting at the 2-second mark without re-encoding locally, reducing bandwidth for background loops.

Step 4: Scroll-triggered playback
If you have multiple videos on a page, loading them all at once wastes bandwidth. Using react-intersection-observer, you can play videos only when they're visible.
The Intersection Observer API watches for a video element to enter the viewport. Once it crosses the threshold we set, it starts playback. When the user scrolls past and the video leaves the viewport, it pauses automatically.
Install the package:
npm install react-intersection-observerNow let's create the component:
// components/ScrollVideo.tsx
'use client';
import { useRef, useEffect, useState } from 'react';
import { buildSrc } from '@imagekit/next';
import { useInView } from 'react-intersection-observer';
interface ScrollVideoProps {
path: string;
transformation?: Array<Record<string, string | number>>;
className?: string;
threshold?: number;
}
export function ScrollVideo({
path,
transformation = [{ width: 1280 }],
className = '',
threshold = 0.5,
}: ScrollVideoProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const { ref: containerRef, inView } = useInView({ threshold });
const [isBlocked, setIsBlocked] = useState(false);
const urlEndpoint = process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT!;
const src = buildSrc({ urlEndpoint, src: path, transformation });
const poster = buildSrc({
urlEndpoint,
src: `${path}/ik-thumbnail.jpg`,
transformation: [{ width: 1280 }],
});
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (inView) {
video.play().catch((error) => {
console.warn('Autoplay blocked:', error);
setIsBlocked(true);
});
} else {
video.pause();
}
}, [inView]);
if (isBlocked) {
return (
<div className={className}>
<img src={poster} alt="" className="h-full w-full object-cover" />
</div>
);
}
return (
<div ref={containerRef} className={className}>
<video
ref={videoRef}
src={src}
muted
playsInline
loop
poster={poster}
preload="metadata"
className="h-full w-full object-cover"
/>
</div>
);
}Usage:
<ScrollVideo
path="/product-demo.mp4"
transformation={[{ width: 800 }]}
className="aspect-video"
/>
For tall vertical videos on mobile, the default threshold={0.5} may never trigger because the video is taller than the screen. Pass threshold={0.1} to start playback as soon as 10% enters the viewport.
Video still not playing?
Check these common issues.
1. iOS goes fullscreen instead of playing
You're missing playsInline. iOS requires this attribute to play videos inline. Without it, iOS tries to open the native fullscreen player and quietly fails.
2. Video works sometimes
Check if the iPhone's battery icon is yellow. iOS blocks all autoplay in Low Power Mode. No code can override this. Provide a poster fallback so users still see something.
3. Browser thinks your video has audio
Your video might look silent but still contain an audio track. Use audioCodec: 'none' in your transformation or ac-none in the URL to strip the audio codec entirely.
4. First load is slow
ImageKit caches transformed videos after the first request. The initial transformation may take a few seconds for large files. For production, request your video URLs once before going live. Learn more about first-time request processing.
Conclusion
Getting autoplay to work is only part of the solution. You also need it to load quickly. By combining the right attributes with ImageKit's real-time optimization, you get fast, reliable autoplay that works everywhere.
Sign up for a free ImageKit account and test the ac-none transformation on your own hero section.