D
P
0

CSS & Web Animation

Background `<video>` Flashes Black Repeatedly in Chrome/Brave on Windows? DirectComposition Overlay Plane + Loop Encode

July 28, 2026·4 min read
Background `<video>` Flashes Black Repeatedly in Chrome/Brave on Windows? DirectComposition Overlay Plane + Loop Encode

I was finalizing the hero of an immersive one-pager for a client site. Behind the text sat a full-bleed <video> element that looped continuously, slowly zooming via an animation to keep the background alive. On macOS it was flawless. Then I opened it on Windows in Brave, and a black flash started showing up on repeat. Plain Chrome did the same. Not a subtle flicker, but a clear black blink.

What threw me off first: because the video was being animated, I immediately suspected a CSS transform or an ordinary compositing-layer issue.

The symptom

Looking closely, the black was not random across the viewport. It appeared exactly over the video area, and I could catch remnants of the previous frame lingering at the bottom of the new one. The flash arrived at two distinct moments: every time the video was scaled, moved, or covered by another layer above it, and once more each time the video finished a loop and jumped back to the start.

Those two patterns mattered. Leftover frame at the bottom, plus black confined to the video area, is the telltale sign: this is a decode artifact, not a CSS bug. A pure CSS problem would not leave a slice of the previous frame behind.

Dead ends

I burned time on a few wrong guesses first:

  • Poking at CSS compositing: will-change: transform, backface-visibility: hidden, swapping object-fit values. Nothing changed.
  • Suspecting the GSAP tween was corrupting layer promotion, so I slowed the duration down. The flash stayed.
  • Assuming the video file was corrupt or the codec was odd, so I re-exported from source. Same result.

All dead ends, because I was still treating this as a CSS problem. The leftover-frame tell had already pointed at the GPU decode and compositing path, not the stylesheet.

The root cause

It turned out there were two genuinely separate causes that happened to stack.

First, the DirectComposition overlay plane. On Windows, when the video is hardware-decoded, Chromium promotes it onto a DirectComposition overlay plane. That is a dedicated, power-efficient GPU path for video. The catch: the moment the video is scaled, translated, or occluded by another layer, it drops in and out of that overlay path. Each transition into or out of the overlay produces a fraction-of-a-second black flash. Because my hero was continuously animated (a slow zoom touching the video transform), the video kept leaving and re-entering the overlay nonstop, and every switch was one flash.

Second, a loop seek landing on a dirty frame. A short looping video seeks back to frame 0 every iteration. Default x264 encoding uses B-frames and a long GOP, so the loop start is not necessarily a clean keyframe. When playback seeks back to the start, the decoder lands on a partial frame and briefly shows black. That is the once-per-loop flash, separate from the first cause.

The fix

Since there were two causes, the fix has two parts.

Part one: force the video out of the overlay path. The core trick is to give it a tiny nudge that makes the compositor stop treating the <video> element as a pure overlay candidate, because now it needs blending. filter: brightness(1.001) and opacity: 0.999 are enough, values invisible to the eye but enough to kill overlay promotion. Add a static transform: translateZ(0):

video.hero-bg {
  /* force it off the DirectComposition overlay plane */
  filter: brightness(1.001);
  opacity: 0.999;
  transform: translateZ(0); /* static, never animate this */
}

Just as important: never tween the <video> element directly. Every transform animation on the video itself is what triggers the overlay switching. Wrap the video, then animate the wrapper, so the video element itself stays still:

<div class="fl-zoom">
  <video class="hero-bg" autoplay muted loop playsinline></video>
</div>
// WRONG: tweening the video element directly triggers overlay in/out
// gsap.to(video, { scale: 1.1, duration: 20 });
 
// RIGHT: animate the wrapper, keep the video element still
gsap.to(".fl-zoom", { scale: 1.1, duration: 20, ease: "none" });

Part two: re-encode the loop so the seek back lands on a clean frame. Kill B-frames, shorten the GOP so keyframes are more frequent, and move the moov atom to the front:

ffmpeg -i hero-source.mp4 \
  -c:v libx264 -bf 0 -g 24 -pix_fmt yuv420p \
  -movflags +faststart -an \
  hero-loop.mp4

-bf 0 disables B-frames, -g 24 shortens the GOP so keyframes land more often, and -movflags +faststart puts the metadata up front. After this re-encode, the loop start always falls on a frame that can display immediately, so the per-loop flash disappears.

Together the two fixes wiped out every flash. The hero was smooth again in Brave and Chrome on Windows, and the zoom still ran because the wrapper animates now, not the video.

Takeaways

  • If the black is confined to the video area and leaves a leftover frame behind, it is a decode artifact, not CSS. Stop poking the stylesheet.
  • Chromium on Windows promotes hardware-decoded video onto a DirectComposition overlay plane; scaling, translating, or occlusion makes it flash in and out.
  • Force it off the overlay with filter: brightness(1.001), opacity: 0.999, and a static transform: translateZ(0) on the video element.
  • Never tween the <video> element directly. Wrap it and animate the wrapper.
  • Re-encode the loop with -bf 0 -g 24 -movflags +faststart so the seek back to frame 0 lands on a clean keyframe.