Video in Next.js looks simple at first—drop in a <video> tag and point it at an MP4. That works until you need adaptive bitrate streaming, fast startup times, or playback that doesn't crater Core Web Vitals. Then you're suddenly dealing with transcoding pipelines, HLS manifests, and CDN configuration.
This guide shows you how to add video to a Next.js application the straightforward way: using a video API that handles the infrastructure while you focus on the product.
The native approach (and its limits)
Next.js doesn't include a built-in video component the way it does for images. You use standard HTML:
// app/page.jsx
export default function Page() {
return (
<video width="640" height="360" controls>
<source src="/videos/demo.mp4" type="video/mp4" />
</video>
);
}This works for small, single-file videos. But there are problems:
No adaptive bitrate: The browser downloads the full MP4 resolution regardless of connection speed or screen size. On a slow connection, viewers wait. On mobile, they burn data on unnecessary quality.
No global delivery: Your video serves from wherever your Next.js app is hosted. For a Vercel deployment, that might be one region. Viewers far from that region get slow load times.
Large bundle impact: Self-hosted MP4s in your /public folder ship with your build. A few large videos can significantly slow deployments.
Format limitations: MP4 with H.264 works broadly, but you miss modern codecs like VP9 or AV1 that deliver better quality at lower bitrates.
For production video, you want HLS (HTTP Live Streaming), which breaks video into small chunks at multiple quality levels. The player picks the right quality dynamically based on available bandwidth.
How Mux Video works with Next.js
Mux is a video API that handles encoding, storage, and delivery. You upload a video, Mux processes it into HLS with multiple renditions, and you get back a playback ID you can use anywhere.
The workflow:
- Upload video → Mux encodes it to adaptive HLS
- Get playback ID → A short string that identifies your video
- Embed with Mux Player → A React component optimized for performance
Here's what the integration looks like:
// app/components/VideoPlayer.jsx
'use client';
import MuxPlayer from '@mux/mux-player-react';
export default function VideoPlayer({ playbackId, title }) {
return (
<MuxPlayer
playbackId={playbackId}
streamType="on-demand"
metadata={{
video_title: title,
}}
/>
);
}// app/page.jsx
import VideoPlayer from './components/VideoPlayer';
export default function Page() {
return (
<main>
<h1>Watch the demo</h1>
<VideoPlayer
playbackId="EcHgOK9coz5K4rjSwOkoE7Y7O01201YMIC200RI6lNxnhs"
title="Product Demo"
/>
</main>
);
}The playbackId comes from the Mux API when you create an asset. Mux Player handles HLS playback, adaptive quality switching, and works across all browsers without additional configuration.
Installing Mux Player in Next.js
Add the React package to your project:
npm install @mux/mux-player-reactMux Player is a client component—it needs to run in the browser—so you'll use the 'use client' directive when importing it:
'use client';
import MuxPlayer from '@mux/mux-player-react';For Server Components in the App Router, you have two options:
Option 1: Create a client wrapper component (recommended)
// components/Player.jsx
'use client';
import MuxPlayer from '@mux/mux-player-react';
export default function Player(props) {
return <MuxPlayer {...props} />;
}Then import this wrapper wherever you need video:
// app/video/[id]/page.jsx
import Player from '@/components/Player';
export default async function VideoPage({ params }) {
const video = await getVideo(params.id);
return (
<article>
<h1>{video.title}</h1>
<Player
playbackId={video.playbackId}
streamType="on-demand"
/>
</article>
);
}Option 2: Use dynamic import with ssr: false
import dynamic from 'next/dynamic';
const MuxPlayer = dynamic(
() => import('@mux/mux-player-react'),
{ ssr: false }
);Creating a hero video with Mux autoplay
Hero sections with background video are common on marketing sites. The key requirements: autoplay, muted (required for autoplay to work), and looping.
Mux Player has extended autoplay options that handle browser restrictions gracefully:
'use client';
import MuxPlayer from '@mux/mux-player-react';
import styles from './HeroVideo.module.css';
export default function HeroVideo({ playbackId }) {
return (
<section className={styles.hero}>
<MuxPlayer
playbackId={playbackId}
autoPlay="muted"
loop
muted
playsInline
className={styles.video}
style={{
'--controls': 'none',
}}
/>
<div className={styles.content}>
<h1>Build something great</h1>
<p>Your hero copy here</p>
</div>
</section>
);
}The CSS module for full-bleed background video:
/* HeroVideo.module.css */
.hero {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
.video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: white;
text-align: center;
}The autoPlay="muted" option ensures the video starts muted (required by browsers) and the --controls: none CSS variable hides the player UI for a clean background effect.
Using next-video for simpler integration
For the most Next.js-native experience, there's next-video—a package that works like next/image but for video. It handles uploading to Mux automatically during development.
npm install next-video
npx next-video initThis creates a /videos folder in your project. Drop a video file there:
/videos
└── hero-loop.mp4Then import it directly:
import Video from 'next-video';
import heroLoop from '/videos/hero-loop.mp4';
export default function HeroSection() {
return (
<Video
src={heroLoop}
autoPlay
muted
loop
playsInline
controls={false}
/>
);
}When you run your development server, next-video uploads the file to Mux and stores the playback ID locally. In production, it serves from Mux's CDN. You get the simplicity of local files with the performance of a video API.
Embedding adaptive HLS in Next.js with Mux
Under the hood, Mux Player streams video via HLS—the industry standard for adaptive streaming. You can also use the raw HLS URL directly if you need to integrate with other players:
https://stream.mux.com/{PLAYBACK_ID}.m3u8For example, with Video.js or HLS.js:
'use client';
import { useEffect, useRef } from 'react';
import Hls from 'hls.js';
export default function HlsPlayer({ playbackId }) {
const videoRef = useRef(null);
useEffect(() => {
const video = videoRef.current;
const src = `https://stream.mux.com/${playbackId}.m3u8`;
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari supports HLS natively
video.src = src;
} else if (Hls.isSupported()) {
// Use HLS.js for other browsers
const hls = new Hls();
hls.loadSource(src);
hls.attachMedia(video);
}
}, [playbackId]);
return <video ref={videoRef} controls />;
}However, Mux Player bundles HLS.js internally with optimized configuration, so you get the best performance without managing the library yourself.
Integrating Mux with Vercel deployments
Mux is available in the Vercel Marketplace, making integration with Vercel-hosted Next.js apps straightforward:
- Go to your Vercel dashboard → Storage → Browse Marketplace
- Find Mux and connect it to your project
- Environment variables (MUX_TOKEN_ID, MUX_TOKEN_SECRET) are added automatically
Once connected, you can use the Mux API from your Next.js Server Actions or Route Handlers:
// app/api/videos/route.js
import Mux from '@mux/mux-node';
const mux = new Mux();
export async function POST(request) {
const { videoUrl } = await request.json();
const asset = await mux.video.assets.create({
input: [{ url: videoUrl }],
playback_policy: ['public'],
});
return Response.json({
playbackId: asset.playback_ids[0].id,
status: asset.status,
});
}For direct uploads from the browser (user-generated content), create an upload URL server-side and pass it to the client:
// app/api/upload/route.js
import Mux from '@mux/mux-node';
const mux = new Mux();
export async function POST() {
const upload = await mux.video.uploads.create({
new_asset_settings: {
playback_policy: ['public'],
},
cors_origin: '*',
});
return Response.json({
uploadUrl: upload.url,
uploadId: upload.id,
});
}Adding captions to your videos
Mux generates captions automatically during encoding when you enable the feature:
const asset = await mux.video.assets.create({
input: [{ url: videoUrl }],
playback_policy: ['public'],
auto_generated_captions: [{ language: 'en' }],
});Mux Player displays captions automatically when they're available. You can control their initial visibility:
<MuxPlayer
playbackId={playbackId}
defaultHiddenCaptions={false} // Show captions by default
/>For custom caption files (WebVTT), use the text tracks API after the asset is created.
Performance considerations for Next.js
Video can impact Core Web Vitals if handled poorly. Here's how to keep things fast:
Use loading="lazy" for below-fold videos
Mux Player supports lazy loading to defer video initialization until the player enters the viewport:
<MuxPlayer
playbackId={playbackId}
loading="viewport"
/>Provide poster images
Show a static image while the video loads. Mux generates these automatically:
<MuxPlayer
playbackId={playbackId}
poster={`https://image.mux.com/${playbackId}/thumbnail.jpg`}
/>Consider preload settings
For hero videos that should start fast, use preload="auto". For other videos, preload="metadata" or preload="none" reduces initial bandwidth:
<MuxPlayer
playbackId={playbackId}
preload="metadata"
/>Use Server Components for data fetching
Fetch video metadata (titles, playback IDs) in Server Components, then pass to client components:
// app/videos/page.jsx (Server Component)
import { getVideos } from '@/lib/videos';
import VideoGrid from './VideoGrid';
export default async function VideosPage() {
const videos = await getVideos();
return <VideoGrid videos={videos} />;
}
// VideoGrid.jsx (Client Component)
'use client';
import MuxPlayer from '@mux/mux-player-react';
export default function VideoGrid({ videos }) {
// Render players with data from server
}Streaming analytics with Mux Data
Mux Player automatically reports playback analytics to Mux Data. You get metrics like:
- Video startup time
- Rebuffering rate
- Playback errors
- Watch time and engagement
Add metadata to track custom dimensions:
<MuxPlayer
playbackId={playbackId}
metadata={{
video_id: 'product-demo-2024',
video_title: 'Product Demo',
viewer_user_id: user.id,
}}
/>The analytics appear in your Mux dashboard without additional configuration.
FAQ
How can I add video streaming features to my Next.js application?
Install @mux/mux-player-react, create a client component wrapper, and use <MuxPlayer playbackId="..." /> to embed videos. Mux handles encoding, adaptive streaming, and global delivery. Upload videos via the Mux API or use next-video for automatic handling during development.
What's the easiest way to embed an adaptive HLS video player in Next.js?
Use Mux Player, which handles HLS internally. Install the package, pass your playback ID, and the component manages quality switching, browser compatibility, and playback optimization automatically. No HLS.js configuration required.
What's the easiest way to integrate video streaming into a Vercel-hosted website?
Connect Mux through the Vercel Marketplace. This adds API credentials as environment variables automatically. Then use the Mux API from Server Actions or Route Handlers to create assets and get playback IDs for your frontend.
How can I use a video API to embed a muted, autoplaying hero video in Next.js?
Use Mux Player with autoPlay="muted", loop, muted, and playsInline props. Hide controls with style={{ '--controls': 'none' }}. Position the player absolutely within a container and overlay your content on top with position: relative and z-index.
How do I implement video playback in Next.js with an easy-to-use API?
Upload videos to Mux (via API or next-video), get a playback ID, and embed with <MuxPlayer playbackId="..." />. The player handles HLS streaming, captions, accessibility, and analytics out of the box. Server Components fetch metadata; client components render the player.