Behind YouTube’s Ambient Glow

YouTube’s “Ambient Mode” is a subtle but striking feature: as a video plays, the surrounding dark background gently takes on the video’s dominant colors. Officially described by YouTube as using “a lighting effect to make watching videos in the Dark theme more immersive,” it adds depth without distracting from the content.

Peeking under the hood reveals the implementation is not a complex color-sampling engine but rather a clever use of standard web primitives: an HTML <canvas> element synced with the video, a touch of blur, and some CSS positioning. The core idea is to display a heavily downscaled version of the current video frame in a canvas placed behind the player. This low-resolution snapshot preserves the frame’s dominant hues while discarding fine details. A blur filter then smooths the blocky pixels into a soft glow.

Here’s how to recreate that effect from scratch.

Why Canvas Wins for This Job

While SVG could theoretically draw the same shapes, <canvas> is the better tool here for two key reasons. First, it is more performant because it doesn’t require additional DOM nodes for drawing. Second, it is straightforward to update each frame, making it ideal for syncing with a playing video.

The element’s accessibility is a downside; because content is drawn via JavaScript without DOM updates, it requires extra effort to make it accessible. For a decorative effect like this, hiding the canvas from assistive devices is sufficient, but you should still disable the effect for users with reduced-motion preferences.

A <canvas> is defined by its width and height attributes, which set its coordinate system. These are CSS pixels, so stretching a small canvas in a larger container will result in a pixelated image—which is precisely what we want here, as it will serve as the basis for our glow.

Keeping It in Sync

The <canvas> element handles rendering, but we need to keep it updated with each video frame. Using setInterval at 60fps is possible but problematic. The better approach is the requestAnimationFrame method, which instructs the browser to run a specified function before the next repaint. It runs asynchronously and returns a request ID that can be used with cancelAnimationFrame to stop the loop.

The process involves downscaling the video frame to a small canvas—for instance, drawing a 1920×720 video frame onto a 10×6 pixel canvas. This automatic downscaling acts like a primitive color sampler by preserving only the frame’s prominent, dominant color regions. That small, pixelated image is then scaled up and blurred behind the video.

Setting Up the Structure

We need a parent container to hold both the<video> and the <canvas> elements. This wrapper allows us to contain the absolutely positioned canvas behind the video. The video frame size defines our working space; for a 1920×720 video, we set the canvas coordinate system so we can draw a heavily scaled-down version of it.

All the update events are tied directly to the video’s playback state. The specific events we need to handle are:

  • loadeddata: fires when the first frame loads, triggering a one-time draw.
  • seeked: fires when seeking is complete, triggering a one-time draw.
  • play: fires when playback starts, starting the update loop.
  • pause: fires when paused, stopping the update loop.
  • ended: fires at the end, stopping the update loop.

The drawing function uses drawImage, passing it the video element and four coordinates. This draws the current video frame directly onto the canvas. Looping this function with requestAnimationFrame while the video plays creates a smooth, synchronized background update.

Blur, Position, and Style

A blur() filter can be applied directly to the canvas’s rendering context, blending the low-resolution pixels. Rather than introducing complex computations, this blurs the sampled colors just enough to create a smooth glow.

The actual placement is handled by CSS. Absolute positioning places the canvas behind the video. An opacity setting tones the glow down so it remains subtle, and an inset shadow on the wrapper helps soften the boundary between the video and its background.

The resulting visible effect is a convincing replica of YouTube’s implementation. While the team likely used a proprietary algorithm or extra transitions, this approach achieves a near-identical result with only a few lines of core code. To ensure the solution scales across multiple videos, all the logic can be wrapped into a reusable ES6 class. A new instance can then be created simply by passing in an id for a video and canvas pairing.

Implementing Reduced Motion

Respecting user preferences is a requirement, not a nice-to-have. The prefers-reduced-motion CSS media query allows us to hide the canvas entirely for users who have requested fewer motion effects. Alternatively, JavaScript’s matchMedia function can be used to detect the same preference and simply skip registering the animation event listeners, preventing the loop from ever starting.

Porting the Effect to React

Moving the implementation into a React project requires a different approach to DOM access and lifecycle management. Instead of getElementById, we use useRef hooks to attach references to the <canvas> and <video> elements.

Building a Custom Hook

A custom hook handles the setup and teardown of the effect. The useEffect hook is used to initialize the event listeners once the elements have mounted, and to clean them up on unmount. The hook returns the two ref values that will be assigned to the respective elements in the component.

import { useRef, useEffect } from "react";

export const useVideoBackground = () => {
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
  const canvasRef = useRef();
  const videoRef = useRef();
  
  const init = () => {
    const video = videoRef.current;
    const canvas = canvasRef.current;
    let step;
    
    if (mediaQuery.matches) {
      return;
    }
    
    const ctx = canvas.getContext("2d");
    
    ctx.filter = "blur(1px)";
    
    const draw = () => {
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    };
    
    const drawLoop = () => {
      draw();
      step = window.requestAnimationFrame(drawLoop);
    };
    
    const drawPause = () => {
      window.cancelAnimationFrame(step);
      step = undefined;
    };
    
    // Initialize
    video.addEventListener("loadeddata", draw, false);
    video.addEventListener("seeked", draw, false);
    video.addEventListener("play", drawLoop, false);
    video.addEventListener("pause", drawPause, false);
    video.addEventListener("ended", drawPause, false);
    
    // Run cleanup on unmount event
    return () => {
      video.removeEventListener("loadeddata", draw);
      video.removeEventListener("seeked", draw);
      video.removeEventListener("play", drawLoop);
      video.removeEventListener("pause", drawPause);
      video.removeEventListener("ended", drawPause);
    };
  };
  
  useEffect(init, []);
  
  return {
    canvasRef,
    videoRef,
  };
};

Component Structure

The component itself uses the custom hook and assigns the returned ref values. To make it reusable, the component accepts any <video> element attribute as a prop, such as src.

import React from "react";
import { useVideoBackground } from "../hooks/useVideoBackground";

import "./VideoWithBackground.css";

export const VideoWithBackground = (props) => {
  const { videoRef, canvasRef } = useVideoBackground();
  
  return (
    <section className="wrapper">
      <video ref={ videoRef } controls className="video" { ...props } />
      <canvas width="10" height="6" aria-hidden="true" className="canvas" ref={ canvasRef } />
    </section>
  );
};

Usage is straightforward: pass a video URL to the component as a prop.

import { VideoWithBackground } from "../components/VideoWithBackground";

function App() {
  return (
    <VideoWithBackground src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" />
  );
}

export default App;

Wrapping Up

The Ambient Mode effect is a combination of the HTML <canvas> element, the Canvas API, and JavaScript's requestAnimationFrame method. By drawing the current <video> frame onto the <canvas>, keeping both elements synchronized, and positioning the blurred canvas behind the video, we replicate the visual behavior that makes YouTube's feature distinctive.

Several practical considerations were addressed along the way. The <canvas> is treated as a decorative element that can be removed or hidden when a user prefers reduced motion. For maintainability, the logic was encapsulated in a reusable ES6 class that supports multiple instances on a single page. Finally, the React component version offers a way to integrate the effect into component-based projects.

You can experiment with the finished demo and build on top of it. If you have questions or create your own variation, you can reach out to the author on Twitter.

References