📺 Youtube Background
ESM module for turning a YouTube, Vimeo or plain video file link into a cover background — with autoplay, looping, play/mute controls, seek bars and playlist groups.
npm install youtube-background
Important
2.0 removed the jQuery plugin and renamed the bundles. The factory and every option,
event and method are unchanged — jquery.youtube-background.js is now
dist/youtube-background.js, and youtube-background-experimental.js is
dist/youtube-background-controls.js. The 1.x page is here.
Note
If you would rather write markup than call a constructor, the same three providers ship
as a custom element in
video-background-element —
<video-background src="…">, no factory and no selector. It is where new features go;
this package stays maintained for the [data-vbg] API.
Live demos
Every band below is a real instance. The markup that produces it is right under it.
YouTube
Autoplaying, muted, looping, and paused whenever it scrolls out of view.
<div data-vbg="https://www.youtube.com/watch?v=eEpEeyqGlxA" data-vbg-load-background="true"></div>
A slice of a video
start-at skips the intro, end-at keeps the loop short. The play button is the plugin's own.
<div
data-vbg="https://www.youtube.com/watch?v=MgDZBqTuUuE"
data-vbg-play-button="true"
data-vbg-start-at="10"
data-vbg-end-at="16"
></div>
Sound, and your own controls
Unmute with the plugin's button, or drive it from buttons of your own — those keep their name and carry the state in aria-pressed.
Vimeo
Same attributes, different platform. Unlisted links keep their hash.
<div data-vbg="https://vimeo.com/137250145" data-vbg-mute-button="true"></div>
<!-- unlisted -->
<div data-vbg="https://vimeo.com/304887422/34c51f7a09"></div>
A plain video file
Any .mp4, .webm, .ogg, .avi, .mov, .m4v or .qt URL, played in a native <video>.
<div
data-vbg="https://media.w3.org/2010/05/sintel/trailer.mp4"
data-vbg-poster="https://media.w3.org/2010/05/sintel/poster.png"
data-vbg-start-at="10"
data-vbg-end-at="25"
></div>
A group
Three backgrounds across all three source types, played as one playlist.
<div class="js-vbg-group">
<div data-vbg="https://www.youtube.com/watch?v=LC5rEhxGqT4" data-vbg-loop="false" data-vbg-load-background="true"></div>
<div data-vbg="https://vimeo.com/137250145" data-vbg-loop="false" data-vbg-autoplay="false" data-vbg-load-background="true" style="display: none"></div>
<div data-vbg="https://media.w3.org/2010/05/bunny/trailer.mp4" data-vbg-loop="false" data-vbg-autoplay="false" data-vbg-end-at="20" style="display: none"></div>
</div>
A group only toggles display between its children, so stacking them is your CSS: the
demo above absolutely positions all three over the same box and hides every one but the
first. loop="false" is what makes a member end, and an ended member is what advances
the group, so a looping member would hold the playlist forever.
The video files are the Blender Foundation's Sintel and
Big Buck Bunny trailers, CC-BY 3.0, served from
media.w3.org.
Usage
Point an empty element at a video and initialise:
import { VideoBackgrounds } from "youtube-background";
new VideoBackgrounds("[data-vbg]");
Or from a script tag, where the bundle exposes window.VideoBackgrounds:
<script src="youtube-background.min.js"></script>
The seek bars, play/mute toggles and group controls used above are optional and live in
a bundle of their own, with an optional stylesheet. Each control is a class over markup
you write, pointed at a background by data-target, and each has a destroy() that takes
its listeners back:
<link rel="stylesheet" href="youtube-background-controls.min.css">
<script src="youtube-background-controls.min.js"></script>
Options
Every option is settable as a data-vbg-* attribute on the element, or as a key in
the object passed to the constructor. Attributes win.
| Option | Default | What it does |
|---|---|---|
autoplay |
true |
Start as soon as the element is in view |
muted |
true |
Start muted — required for autoplay to be allowed |
loop |
true |
Restart when the video ends |
mobile |
true |
Create the background on mobile too |
always-play |
false |
Keep playing while off-screen |
start-at |
0 |
Seconds to start from |
end-at |
0 |
Seconds to stop at, 0 for the full duration |
volume |
1 |
0–1, applied on first unmute |
play-button |
false |
Render the plugin's play/pause toggle |
mute-button |
false |
Render the plugin's mute toggle |
poster |
null |
Image shown until the first frame plays |
load-background |
false |
Use the platform's own thumbnail as the poster |
resolution |
'16:9' |
Aspect ratio used to cover the container |
fit-box |
false |
Stretch to the container instead of covering it |
inline-styles |
true |
Let the plugin write the positioning styles |
no-cookie |
true |
Use the privacy-preserving embed domains |
lazyloading |
false |
Add loading="lazy" to the iframe — YouTube and Vimeo only |
title |
'Video background' |
Accessible name for the player frame |
Limits
Since May 2026 (#77),
YouTube's player flashes its own round play/pause icon in the middle of the frame on every
playback toggle — .ytp-bezel, drawn inside the iframe, which the embed's controls=0
does not cover. The frame is cross-origin, so neither your CSS nor your script reaches it,
and the plugin's own play and mute buttons set it off like any other toggle. There is no
option that turns it off, here or upstream.
Cosmetic filtering hides it — an element-hiding rule in Adblock Plus / uBlock Origin
syntax, where ## means hide this selector on these domains:
www.youtube-nocookie.com,www.youtube.com##.html5-video-player .ytp-bezel
Both domains, because no-cookie defaults to true and puts the player on
www.youtube-nocookie.com.
Warning
That fixes the browser it is typed into and nothing further. A content blocker is an
extension, and injecting a stylesheet into a cross-origin frame is a permission
extensions have and pages do not — your CSS never enters the frame,
iframe.contentDocument throws. So it is a development comfort on localhost, and every
visitor without that filter still sees the bezel.
Events
Every instance dispatches on its own element and bubbles:
video-background-ready, video-background-play, video-background-pause,
video-background-ended, video-background-seeked, video-background-time-update,
video-background-state-change, video-background-mute, video-background-unmute,
video-background-volume-change, video-background-resize,
video-background-destroyed.
document.querySelector("#hero").addEventListener("video-background-ready", (event) => {
console.log(event.detail.type, event.detail.currentState);
});
A group dispatches on the group element, with the group in event.detail:
video-background-group-play, video-background-group-pause,
video-background-group-mute, video-background-group-unmute,
video-background-group-next, video-background-group-previous,
video-background-group-forward-rewind, video-background-group-backward-rewind.
The last two fire when stepping past either end of the stack wraps around. Up to
and including 1.2.0 they never fired, and the unmute event was dispatched under
the misspelling video-background-group-umnute.
API
const backgrounds = new VideoBackgrounds("[data-vbg]");
const instance = backgrounds.get(document.querySelector("#hero"));
instance.play();
instance.pause();
instance.mute();
instance.unmute();
instance.setVolume(0.4);
instance.seek(50); // percent
instance.seekTo(12); // seconds
instance.setSource("https://vimeo.com/137250145");
backgrounds.pauseAll();
backgrounds.playAll();
backgrounds.add(element);
backgrounds.destroy(element);
backgrounds.destroyAll();