Rejouice — loading0

Poster-to-loop video

A silent looping video that shows a matching poster frame until it can actually play, and never blocks the page on itself.

Grade: ARuntimes: cssStatus: code-only

Documented, no live demo yet — the code below is complete.

Intent

Background video carries motion that a still cannot, but it is also the single heaviest thing on most pages and the most likely to fail. Making the poster frame the first-class state means the section is complete before the video arrives, and stays complete if it never does.

Implementation

The `<source>` carries `data-src` rather than `src`, so nothing is fetched until the element is near the viewport.

Dependencies: none

html-css-js
<figure class="loop">
  <video
    class="loop__video"
    poster="/media/studio-poster.jpg"
    width="1920"
    height="1080"
    muted
    loop
    playsinline
    preload="metadata"
    aria-hidden="true"
    data-lazy-video
  >
    <source data-src="/media/studio-loop.mp4" type="video/mp4" />
  </video>
</figure>

<style>
.loop { margin: 0; }

.loop__video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--frame-radius, 0);
}
</style>

<script type="module">
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;

// Under reduced motion the poster is the finished state — nothing is fetched.
if (!reduce) {
  const io = new IntersectionObserver(
    (entries) => {
      for (const entry of entries) {
        const video = entry.target;

        if (!entry.isIntersecting) {
          video.pause();
          continue;
        }

        const source = video.querySelector("source[data-src]");
        if (source) {
          source.src = source.dataset.src;
          source.removeAttribute("data-src");
          video.load();
        }

        video.play().catch(() => {
          /* Autoplay refused: the poster remains, which is a valid final state. */
        });
      }
    },
    { rootMargin: "20% 0px" },
  );

  for (const video of document.querySelectorAll("[data-lazy-video]")) io.observe(video);

  addEventListener("visibilitychange", () => {
    if (document.hidden) {
      for (const video of document.querySelectorAll("[data-lazy-video]")) video.pause();
    }
  });
}
</script>

Use it when

  • Hero backgrounds, case-study process footage, product loops under ten seconds.

Avoid it when

  • For anything with information in the audio, which must then be a real player with controls and captions.
  • On more than one section per page — the bandwidth cost compounds.

Accessibility

  • Decorative background video takes `aria-hidden` and no controls. Video that carries meaning needs controls, captions, and a transcript — at which point this pattern is the wrong one.
  • Under reduced motion, do not autoplay. Show the poster and offer a play control if the video matters.
  • Text over the video needs the `--veil` scrim and a contrast check against the brightest frame, not a representative one.

Performance

  • `preload="metadata"` — never `auto` for a decorative loop.
  • Attach `src` lazily via IntersectionObserver so off-screen videos cost nothing.
  • Keep loops under about ten seconds and encode them small; the poster carries first paint.
  • Pause when off-screen and on `visibilitychange` — a hidden playing video still burns decode and battery.

Knobs

aspect
Ratio shared by poster and video. Default: 16 / 9
preload
Preload hint. Default: metadata

Composition

Instruction for Claude Code

Add a poster-to-loop background video: a <video> with muted, loop, playsinline, preload="metadata", explicit width/height, a matching poster image, aspect-ratio and object-fit cover, and aria-hidden if decorative. Put the file on a data-src attribute of the <source> and only assign src + call load() when an IntersectionObserver with 20% rootMargin reports it near the viewport; pause when it leaves and on visibilitychange when the document hides. Catch the play() rejection and leave the poster showing — that is a valid final state. Under prefers-reduced-motion, never fetch or autoplay the video at all.