Open demo

CSS code

HTML
<div><span class="lbl">Full-bleed hero with a pause button</span>
<section class="vh vh-full" data-hero-video aria-labelledby="vh-full-title">
  <video src="../media/hero-loop.mp4" poster="../media/hero-loop-poster.jpg" muted loop playsinline preload="metadata" data-bg aria-hidden="true"></video>
  <div class="vh-body">
    <p class="vh-kicker">Northlight Studio · 2026 season</p>
    <h2 id="vh-full-title">Patterns that move on their own</h2>
    <p class="vh-lead">Short films made from simple rules: fractals, cellular automata and drifting colour. A new study every month, free to watch.</p>
    <div class="vh-ctas"><a class="vh-btn primary" href="#">Watch the studies</a><a class="vh-btn ghost" href="#">About the studio</a></div>
  </div>
  <button type="button" class="vh-toggle" data-bg-toggle aria-label="Pause background video"><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><span data-bg-label>Pause</span></button>
</section></div>

<div><span class="lbl">Split hero with a framed loop</span>
<section class="vh vh-split" data-hero-video aria-labelledby="vh-split-title">
  <div>
    <p class="vh-kicker">Generative Motion course</p>
    <h2 id="vh-split-title">Learn to make films from four lines of code</h2>
    <p class="vh-lead">Twelve short lessons on gradients, fractals and cellular automata, each with a finished study to remix.</p>
    <div class="vh-ctas"><a class="vh-btn primary" href="#">Start the course</a><a class="vh-btn ghost" href="#">See the syllabus</a></div>
    <div class="vh-stats"><p><b>12</b><span>lessons</span></p><p><b>3 h 40 min</b><span>of video</span></p><p><b>4.9</b><span>from 1,280 learners</span></p></div>
  </div>
  <div class="vh-frame">
    <video src="../media/hero-loop.mp4" poster="../media/hero-loop-poster.jpg" muted loop playsinline preload="metadata" data-bg aria-hidden="true"></video>
    <button type="button" class="vh-toggle" data-bg-toggle aria-label="Pause background video"><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><span data-bg-label>Pause</span></button>
  </div>
</section></div>
CSS
/* Background video heroes: a muted, looping <video> behind the content, a poster image as the fallback,
   and a pause button. The script toggles [data-playing] on the hero and the button's label. */
.vh{--vh-accent:#4338ca;position:relative;isolation:isolate;overflow:hidden}
.vh video{display:block;width:100%;height:100%;object-fit:cover}
.vh a{text-decoration:none}
.vh-toggle{display:inline-flex;align-items:center;gap:6px;height:38px;padding:0 14px 0 10px;border:1px solid rgba(255,255,255,.45);border-radius:99px;
  background:rgba(15,12,41,.55);color:#fff;font:inherit;font-size:.82rem;font-weight:600;cursor:pointer;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
.vh-toggle:hover{background:rgba(15,12,41,.75)}
.vh-toggle:focus-visible{outline:2px solid #fff;outline-offset:2px}
.vh-toggle svg{width:18px;height:18px}
.vh .i-pause,.vh[data-playing] .i-play{display:none}
.vh[data-playing] .i-pause{display:block}
/* 1. full-bleed hero */
.vh-full{display:grid;align-items:end;min-height:520px;border-radius:26px;background:#1e1b4b;color:#fff}
.vh-full video{position:absolute;inset:0;z-index:-2}
.vh-full::before{content:"";position:absolute;inset:0;z-index:-1;background:linear-gradient(to top,rgba(15,12,41,.92),rgba(15,12,41,.55) 55%,rgba(15,12,41,.25))}
.vh-body{max-width:640px;padding:48px}
.vh-kicker{font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#e0e7ff}
.vh-full h2{margin-top:12px;font-size:clamp(2rem,5vw,3.4rem);line-height:1.05;letter-spacing:-.03em}
.vh-full p.vh-lead{margin-top:16px;font-size:1.06rem;line-height:1.6;color:#e2e8f0}
.vh-ctas{display:flex;flex-wrap:wrap;gap:12px;margin-top:26px}
.vh-btn{display:inline-flex;align-items:center;height:48px;padding:0 22px;border-radius:99px;font-weight:700;font-size:.95rem}
.vh-btn.primary{background:#fff;color:#1e1b4b}
.vh-btn.primary:hover{background:#e0e7ff}
.vh-btn.ghost{border:1.5px solid rgba(255,255,255,.7);color:#fff}
.vh-btn.ghost:hover{background:rgba(255,255,255,.12)}
.vh-btn:focus-visible{outline:2px solid #fff;outline-offset:3px}
.vh-full .vh-toggle{position:absolute;right:20px;bottom:20px}
/* 2. split hero with a framed loop */
.vh-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:44px;align-items:center;overflow:visible}
.vh-split h2{margin-top:12px;font-size:clamp(1.9rem,4vw,2.8rem);line-height:1.08;letter-spacing:-.03em;color:#1e1b4b}
.vh-split .vh-kicker{color:var(--vh-accent)}
.vh-split p.vh-lead{margin-top:14px;font-size:1.02rem;line-height:1.6;color:#4a4769}
.vh-split .vh-btn.primary{background:var(--vh-accent);color:#fff}
.vh-split .vh-btn.primary:hover{background:#3730a3}
.vh-split .vh-btn.ghost{border-color:#c7c4dd;color:#1e1b4b}
.vh-split .vh-btn.ghost:hover{background:#eeedf8}
.vh-split .vh-btn:focus-visible{outline-color:var(--vh-accent)}
.vh-frame{position:relative;overflow:hidden;border-radius:26px;background:#1e1b4b;aspect-ratio:4/3;box-shadow:0 24px 50px rgba(30,27,75,.22)}
.vh-frame .vh-toggle{position:absolute;left:16px;bottom:16px}
.vh-stats{display:flex;gap:28px;margin-top:28px}
.vh-stats b{display:block;font-size:1.4rem;color:#1e1b4b}
.vh-stats span{font-size:.82rem;color:#5b5878}
@media (max-width:760px){.vh-split{grid-template-columns:1fr;gap:28px}.vh-body{padding:28px 24px 84px}.vh-full{min-height:480px}}
JavaScript
(() => {
// Background video heroes. The <video> is muted, loops and has no autoplay attribute: the script starts it
// only when the visitor has not asked for reduced motion, pauses it while the hero is off screen, and wires
// the pause button (WCAG 2.2.2). Without JavaScript, or with reduced motion, the poster image stays.
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('[data-hero-video]').forEach(root => {
  const video = root.querySelector('video'), btn = root.querySelector('[data-bg-toggle]');
  const text = btn.querySelector('[data-bg-label]');
  let choice = null, visible = true; // choice: 'play' or 'pause' once the visitor has pressed the button
  const label = () => {
    const playing = !video.paused;
    root.toggleAttribute('data-playing', playing);
    btn.setAttribute('aria-label', (playing ? 'Pause' : 'Play') + ' background video');
    if (text) text.textContent = playing ? 'Pause' : 'Play';
  };
  const update = () => {
    const wanted = choice ? choice === 'play' : !reduce.matches;
    if (wanted && visible) video.play().catch(() => {}); else video.pause();
  };
  btn.addEventListener('click', () => { choice = video.paused ? 'play' : 'pause'; update(); });
  video.addEventListener('play', label);
  video.addEventListener('pause', label);
  reduce.addEventListener('change', update);
  if ('IntersectionObserver' in window)
    new IntersectionObserver(([e]) => { visible = e.isIntersecting; update(); }).observe(root);
  label();
  update();
});
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Full-bleed hero with a pause button</span>
<section class="vh vh-full position-relative overflow-hidden d-grid align-items-end text-white" data-hero-video aria-labelledby="vh-full-title">
  <video src="../media/hero-loop.mp4" poster="../media/hero-loop-poster.jpg" muted loop playsinline preload="metadata" data-bg aria-hidden="true"></video>
  <div class="vh-body">
    <p class="vh-kicker fw-bold text-uppercase mb-0">Northlight Studio · 2026 season</p>
    <h2 class="fw-bold mt-3 mb-0" id="vh-full-title">Patterns that move on their own</h2>
    <p class="vh-lead mt-3 mb-0">Short films made from simple rules: fractals, cellular automata and drifting colour. A new study every month, free to watch.</p>
    <div class="d-flex flex-wrap gap-2 mt-4"><a class="btn btn-light btn-lg rounded-pill" href="#">Watch the studies</a><a class="btn btn-outline-light btn-lg rounded-pill" href="#">About the studio</a></div>
  </div>
  <button type="button" class="vh-toggle btn rounded-pill position-absolute d-inline-flex align-items-center gap-1" data-bg-toggle aria-label="Pause background video"><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><span data-bg-label>Pause</span></button>
</section></div>

<div><span class="lbl">Split hero with a framed loop</span>
<section class="vh vh-split row g-4 g-lg-5 align-items-center" data-hero-video aria-labelledby="vh-split-title">
  <div class="col-md-6">
    <p class="vh-kicker fw-bold text-uppercase mb-0">Generative Motion course</p>
    <h2 class="fw-bold mt-3 mb-0" id="vh-split-title">Learn to make films from four lines of code</h2>
    <p class="vh-lead mt-3 mb-0">Twelve short lessons on gradients, fractals and cellular automata, each with a finished study to remix.</p>
    <div class="d-flex flex-wrap gap-2 mt-4"><a class="btn btn-primary btn-lg rounded-pill" href="#">Start the course</a><a class="btn btn-outline-secondary btn-lg rounded-pill" href="#">See the syllabus</a></div>
    <div class="vh-stats d-flex gap-4 mt-4"><p class="mb-0"><b class="d-block">12</b><span>lessons</span></p><p class="mb-0"><b class="d-block">3 h 40 min</b><span>of video</span></p><p class="mb-0"><b class="d-block">4.9</b><span>from 1,280 learners</span></p></div>
  </div>
  <div class="col-md-6">
    <div class="vh-frame position-relative overflow-hidden">
      <video src="../media/hero-loop.mp4" poster="../media/hero-loop-poster.jpg" muted loop playsinline preload="metadata" data-bg aria-hidden="true"></video>
      <button type="button" class="vh-toggle btn rounded-pill position-absolute d-inline-flex align-items-center gap-1" data-bg-toggle aria-label="Pause background video"><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><span data-bg-label>Pause</span></button>
    </div>
  </div>
</section></div>
CSS
/* Background video heroes on Bootstrap 5: .btn pills, a .row split layout and a small stylesheet for the video layer,
   the scrim and the pause button. The script toggles [data-playing] on the hero and the button's label. */
.vh{--vh-accent:#4338ca;isolation:isolate}
.vh video{display:block;width:100%;height:100%;object-fit:cover}
.vh-toggle{--bs-btn-color:#fff;--bs-btn-hover-color:#fff;--bs-btn-active-color:#fff;--bs-btn-bg:rgba(15,12,41,.55);--bs-btn-hover-bg:rgba(15,12,41,.75);
  --bs-btn-active-bg:rgba(15,12,41,.85);--bs-btn-border-color:rgba(255,255,255,.45);--bs-btn-hover-border-color:rgba(255,255,255,.6);--bs-btn-active-border-color:#fff;
  --bs-btn-font-size:.82rem;--bs-btn-font-weight:600;--bs-btn-padding-x:14px;--bs-btn-padding-y:0;--bs-btn-focus-box-shadow:none;
  height:38px;padding-left:10px;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
.vh-toggle:focus-visible{outline:2px solid #fff;outline-offset:2px}
.vh-toggle svg{width:18px;height:18px}
.vh .i-pause,.vh[data-playing] .i-play{display:none}
.vh[data-playing] .i-pause{display:block}
.vh .btn-lg{--bs-btn-font-size:.95rem;--bs-btn-font-weight:700;--bs-btn-padding-x:22px;--bs-btn-focus-box-shadow:none;height:48px;display:inline-flex;align-items:center}
.vh .btn-lg:focus-visible{outline:2px solid currentColor;outline-offset:3px}
.vh-kicker{font-size:.78rem;letter-spacing:.14em}
/* 1. full-bleed */
.vh-full{min-height:520px;border-radius:26px;background:#1e1b4b}
.vh-full video{position:absolute;inset:0;z-index:-2}
.vh-full::before{content:"";position:absolute;inset:0;z-index:-1;background:linear-gradient(to top,rgba(15,12,41,.92),rgba(15,12,41,.55) 55%,rgba(15,12,41,.25))}
.vh-full .vh-kicker{color:#e0e7ff}
.vh-full h2{font-size:clamp(2rem,5vw,3.4rem);line-height:1.05;letter-spacing:-.03em}
.vh-full .vh-lead{font-size:1.06rem;line-height:1.6;color:#e2e8f0}
.vh-full .btn-light{--bs-btn-color:#1e1b4b;--bs-btn-hover-bg:#e0e7ff;--bs-btn-hover-border-color:#e0e7ff}
.vh-full .btn-outline-light{--bs-btn-border-color:rgba(255,255,255,.7);--bs-btn-hover-bg:rgba(255,255,255,.12);--bs-btn-hover-color:#fff;border-width:1.5px}
.vh-body{max-width:640px;padding:48px}
.vh-full .vh-toggle{right:20px;bottom:20px}
/* 2. split */
.vh-split .vh-kicker{color:var(--vh-accent)}
.vh-split h2{font-size:clamp(1.9rem,4vw,2.8rem);line-height:1.08;letter-spacing:-.03em;color:#1e1b4b}
.vh-split .vh-lead{font-size:1.02rem;line-height:1.6;color:#4a4769}
.vh-split .btn-primary{--bs-btn-bg:var(--vh-accent);--bs-btn-border-color:var(--vh-accent);--bs-btn-hover-bg:#3730a3;--bs-btn-hover-border-color:#3730a3;--bs-btn-active-bg:#312e81}
.vh-split .btn-outline-secondary{--bs-btn-color:#1e1b4b;--bs-btn-border-color:#c7c4dd;--bs-btn-hover-bg:#eeedf8;--bs-btn-hover-color:#1e1b4b;--bs-btn-hover-border-color:#c7c4dd;border-width:1.5px}
.vh-split .btn-lg:focus-visible{outline-color:var(--vh-accent)}
.vh-frame{border-radius:26px;background:#1e1b4b;aspect-ratio:4/3;box-shadow:0 24px 50px rgba(30,27,75,.22)}
.vh-frame .vh-toggle{left:16px;bottom:16px}
.vh-stats b{font-size:1.4rem;color:#1e1b4b}
.vh-stats span{font-size:.82rem;color:#5b5878}
@media (max-width:760px){.vh-body{padding:28px 24px 84px}.vh-full{min-height:480px}}
JavaScript
(() => {
// Background video heroes. The <video> is muted, loops and has no autoplay attribute: the script starts it
// only when the visitor has not asked for reduced motion, pauses it while the hero is off screen, and wires
// the pause button (WCAG 2.2.2). Without JavaScript, or with reduced motion, the poster image stays.
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('[data-hero-video]').forEach(root => {
  const video = root.querySelector('video'), btn = root.querySelector('[data-bg-toggle]');
  const text = btn.querySelector('[data-bg-label]');
  let choice = null, visible = true; // choice: 'play' or 'pause' once the visitor has pressed the button
  const label = () => {
    const playing = !video.paused;
    root.toggleAttribute('data-playing', playing);
    btn.setAttribute('aria-label', (playing ? 'Pause' : 'Play') + ' background video');
    if (text) text.textContent = playing ? 'Pause' : 'Play';
  };
  const update = () => {
    const wanted = choice ? choice === 'play' : !reduce.matches;
    if (wanted && visible) video.play().catch(() => {}); else video.pause();
  };
  btn.addEventListener('click', () => { choice = video.paused ? 'play' : 'pause'; update(); });
  video.addEventListener('play', label);
  video.addEventListener('pause', label);
  reduce.addEventListener('change', update);
  if ('IntersectionObserver' in window)
    new IntersectionObserver(([e]) => { visible = e.isIntersecting; update(); }).observe(root);
  label();
  update();
});
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b5878] uppercase">Full-bleed hero with a pause button</span>
<section class="group/vh relative isolate grid min-h-[520px] items-end overflow-hidden rounded-[26px] bg-[#1e1b4b] text-white before:absolute before:inset-0 before:-z-1 before:bg-[linear-gradient(to_top,rgba(15,12,41,.92),rgba(15,12,41,.55)_55%,rgba(15,12,41,.25))] max-[760px]:min-h-[480px]" data-hero-video aria-labelledby="vh-full-title">
  <video class="absolute inset-0 -z-2 block size-full object-cover" src="../media/hero-loop.mp4" poster="../media/hero-loop-poster.jpg" muted loop playsinline preload="metadata" data-bg aria-hidden="true"></video>
  <div class="max-w-[640px] p-12 max-[760px]:px-6 max-[760px]:pt-7 max-[760px]:pb-[84px]">
    <p class="text-[.78rem] font-bold tracking-[.14em] text-[#e0e7ff] uppercase">Northlight Studio · 2026 season</p>
    <h2 class="mt-3 text-[clamp(2rem,5vw,3.4rem)] leading-[1.05] font-bold tracking-[-.03em]" id="vh-full-title">Patterns that move on their own</h2>
    <p class="mt-4 text-[1.06rem] leading-[1.6] text-[#e2e8f0]">Short films made from simple rules: fractals, cellular automata and drifting colour. A new study every month, free to watch.</p>
    <div class="mt-[26px] flex flex-wrap gap-3"><a class="inline-flex h-12 items-center rounded-full bg-white px-[22px] text-[.95rem] font-bold text-[#1e1b4b] hover:bg-[#e0e7ff] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-white" href="#">Watch the studies</a><a class="inline-flex h-12 items-center rounded-full border-[1.5px] border-white/70 px-[22px] text-[.95rem] font-bold text-white hover:bg-white/12 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-white" href="#">About the studio</a></div>
  </div>
  <button type="button" class="absolute right-5 bottom-5 inline-flex h-[38px] cursor-pointer items-center gap-1.5 rounded-full border border-white/45 bg-[rgba(15,12,41,.55)] pr-3.5 pl-2.5 text-[.82rem] font-semibold text-white backdrop-blur-[8px] hover:bg-[rgba(15,12,41,.75)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" data-bg-toggle aria-label="Pause background video"><svg class="hidden size-[18px] group-data-playing/vh:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg><svg class="block size-[18px] group-data-playing/vh:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><span data-bg-label>Pause</span></button>
</section></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b5878] uppercase">Split hero with a framed loop</span>
<section class="group/vh grid grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] items-center gap-11 max-[760px]:grid-cols-1 max-[760px]:gap-7" data-hero-video aria-labelledby="vh-split-title">
  <div>
    <p class="text-[.78rem] font-bold tracking-[.14em] text-[#4338ca] uppercase">Generative Motion course</p>
    <h2 class="mt-3 text-[clamp(1.9rem,4vw,2.8rem)] leading-[1.08] font-bold tracking-[-.03em] text-[#1e1b4b]" id="vh-split-title">Learn to make films from four lines of code</h2>
    <p class="mt-3.5 text-[1.02rem] leading-[1.6] text-[#4a4769]">Twelve short lessons on gradients, fractals and cellular automata, each with a finished study to remix.</p>
    <div class="mt-[26px] flex flex-wrap gap-3"><a class="inline-flex h-12 items-center rounded-full bg-[#4338ca] px-[22px] text-[.95rem] font-bold text-white hover:bg-[#3730a3] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#4338ca]" href="#">Start the course</a><a class="inline-flex h-12 items-center rounded-full border-[1.5px] border-[#c7c4dd] px-[22px] text-[.95rem] font-bold text-[#1e1b4b] hover:bg-[#eeedf8] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#4338ca]" href="#">See the syllabus</a></div>
    <div class="mt-7 flex gap-7"><p><b class="block text-[1.4rem]">12</b><span class="text-[.82rem] text-[#5b5878]">lessons</span></p><p><b class="block text-[1.4rem]">3 h 40 min</b><span class="text-[.82rem] text-[#5b5878]">of video</span></p><p><b class="block text-[1.4rem]">4.9</b><span class="text-[.82rem] text-[#5b5878]">from 1,280 learners</span></p></div>
  </div>
  <div class="relative isolate aspect-[4/3] overflow-hidden rounded-[26px] bg-[#1e1b4b] shadow-[0_24px_50px_rgba(30,27,75,.22)]">
    <video class="block size-full object-cover" src="../media/hero-loop.mp4" poster="../media/hero-loop-poster.jpg" muted loop playsinline preload="metadata" data-bg aria-hidden="true"></video>
    <button type="button" class="absolute bottom-4 left-4 inline-flex h-[38px] cursor-pointer items-center gap-1.5 rounded-full border border-white/45 bg-[rgba(15,12,41,.55)] pr-3.5 pl-2.5 text-[.82rem] font-semibold text-white backdrop-blur-[8px] hover:bg-[rgba(15,12,41,.75)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" data-bg-toggle aria-label="Pause background video"><svg class="hidden size-[18px] group-data-playing/vh:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg><svg class="block size-[18px] group-data-playing/vh:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><span data-bg-label>Pause</span></button>
  </div>
</section></div>
JavaScript
(() => {
// Background video heroes. The <video> is muted, loops and has no autoplay attribute: the script starts it
// only when the visitor has not asked for reduced motion, pauses it while the hero is off screen, and wires
// the pause button (WCAG 2.2.2). Without JavaScript, or with reduced motion, the poster image stays.
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('[data-hero-video]').forEach(root => {
  const video = root.querySelector('video'), btn = root.querySelector('[data-bg-toggle]');
  const text = btn.querySelector('[data-bg-label]');
  let choice = null, visible = true; // choice: 'play' or 'pause' once the visitor has pressed the button
  const label = () => {
    const playing = !video.paused;
    root.toggleAttribute('data-playing', playing);
    btn.setAttribute('aria-label', (playing ? 'Pause' : 'Play') + ' background video');
    if (text) text.textContent = playing ? 'Pause' : 'Play';
  };
  const update = () => {
    const wanted = choice ? choice === 'play' : !reduce.matches;
    if (wanted && visible) video.play().catch(() => {}); else video.pause();
  };
  btn.addEventListener('click', () => { choice = video.paused ? 'play' : 'pause'; update(); });
  video.addEventListener('play', label);
  video.addEventListener('pause', label);
  reduce.addEventListener('change', update);
  if ('IntersectionObserver' in window)
    new IntersectionObserver(([e]) => { visible = e.isIntersecting; update(); }).observe(root);
  label();
  update();
});
})();

About this set

Two hero sections with a looping background video that stays out of the visitor’s way. The full-bleed hero puts a heading, a lead paragraph and two call-to-action links over a dark gradient scrim on a silent colour-field loop, with a Pause button in the bottom right corner. The split hero puts the copy and three figures on the left and frames the same loop in a rounded card on the right, with its own Pause button. The video element is muted, loops, plays inline and has no autoplay attribute: the script starts it only when the visitor has not asked for reduced motion, and pauses it while the hero is scrolled out of view to save battery. The Pause button satisfies WCAG 2.2.2 for moving content that lasts longer than five seconds; its label changes between Pause background video and Play background video, and the visitor’s choice wins over the reduced-motion default. The poster image shows whenever the video is not playing, so visitors without JavaScript or with reduced motion see a still frame. The demo loop is a silent clip that plays forward and then in reverse, so it has no visible seam.

What’s included

  • Muted, inline, looping background video with a poster fallback
  • Visible Pause and Play button for WCAG 2.2.2
  • No playback under prefers-reduced-motion unless the visitor starts it
  • Pauses while the hero is off screen
  • Full-bleed and split layouts

Accessibility

The video is decorative and hidden from assistive technology, while the heading carries the section's name. The pause button has visible text and an aria-label that names the background video, and it shows a white outline on focus. Under prefers-reduced-motion the video does not start and the poster stays until the visitor presses Play.

Customise it

Change --vh-accent on .vh for the split hero's buttons and kicker, the gradient on .vh-full::before for scrim strength, and min-height on .vh-full for hero height. Bootstrap uses .btn-light, .btn-outline-light and .btn-primary variables; in Tailwind edit the before:bg-[...] gradient and button colours.