Benji Riethmeier
Icon

Building the Netflix Screen Saver

6/9/2024 | 8 min read

When you let Netflix sit for a while, a slideshow pops up over the whole screen that goes through some content you might want to watch on the platform. Every few seconds a new show will move across the screen along with the title of the show and a few of the show’s attributes (e.g. Witty, Drama, Heartfelt, etc.) The screen saver shows until you interact with the screen again.

I wanted to try to build a near identical component using web technologies and learn a bit more about CSS animations specifically along the way.

Note: I built my component using React and this article assumes basic familiarity with CSS and React (e.g., functional components, hooks)

The Final Product

Splish Splash

  • Watery
  • Fun
  • Light

Photo by: Victor Rosario

You can also see this component full screen by idling on this post for 5 minutes without clicking, scrolling, or moving the mouse. If you’re impatient, you can click the button below (when the screen saver is open, you can close it by clicking the screen again).

How It Works

The final product is a React component that functions with CSS Animations and using certain JavaScript events that hook into the CSS Animation lifecycle.

The Basic Animation

The main image in the slideshow is just a background-image on a div. The background-size property is set to 120% to make sure that the image is always a little larger than the screen and therefore always has room to scroll across the screen.

To animate the background image, a keyframes at-rule is used. The rule lists out the different steps of the animation and when they should occur during the run time of the animation.

Since the main image is a background image, I was able to animate it across the screen with the background-position property, which conveniently allows the simple values left and right. When I add in the fading in and out of the image, the full CSS animation for the background looks like this:

@keyframes moveBackgroundImageFromLeftToRightWithFade {
	from {
		opacity: 0;
		background-position: left;
	}
	15% {
		opacity: 1;
	}
	85% {
		opacity: 1;
	}
	to {
		opacity: 0;
		background-position: right;
	}
}

The animation is added to the element with the background image using different animation properties.

.mainImage {
	animation-duration: 12s;
	animation-timing-function: linear; /* Ensures smooth movement across the screen*/
	animation-iteration-count: infinite; /* Makes animation go forever */
	animation-direction: alternate;
	animation-name: moveBackgroundImageFromLeftToRightWithFade;

	/* Or all as one property */
	animation: 12s linear infinite alternate  moveBackgroundImageFromLeftToRightWithFade;
}

An important property to note is the animation-direction property, which is given a value of alternate. This nifty little property removes the need to create a near duplicate rightToLeft keyframes rule. With alternate, the animation first goes through the keyframes rule in order and then goes through in reverse order during the next iteration and then forwards again and so on.

Moving Through Multiple Images

Originally to move through multiple images, I thought I was going to have to write some gross code with setInterval, but as it turns out there are JavaScript events that hook into the animation lifecycle. These events make this task much easier. For this project, I used the animationiteration event, which fires every time the animation iterates (excluding before the first animation iteration starts and after the last animation iteration ends).

Using this event, my component changes the URL of the background image every time the animation iterates.

// screens is a list of remote image URLs
const NetflixScreenSaver = ({ screens }: { screens: string[] }) => {
	const [currentScreen, setCurrentScreen] = setState(0);

	const handleAnimationIteration = () => {
		setCurrentScreen((prev) => (prev + 1) % screens.length);
	}

	return (
		<div 
			className="mainImage"
			style={{ backgroundImage: `url(${screens[currentScreen]})`}}
			onAnimationIteration={handleAnimationIteration}
		/>
	);
}

The combination of the CSS Animation and the animationiteration event finishes the main functionality of the screen.

Adding the Text

From here, we just need to add the text on the screen along with the list of attributes. This is done easily by absolutely positioning a p and ul element at the bottom left of the screen. The text also fades in a little bit after the image does so I created a separate keyframes rule to animate the opacity of the text content.

An interesting feature of the text that I noticed was that there is a mild gradient behind the text. Since the background image could be light and our text is white, a dark gradient behind the text ensures that the text is not hard for users to read.

<div className="mainImage">
	<div className="content">...</div>
	<div className="gradient" />
</div>

<style>
	.gradient {
		position: absolute;
		bottom: 0;
		width: 100%;
		height: 50%;
		background: linear-gradient(to top, black, transparent);
	}
</style>

The linear-gradient value just creates a gradient from the bottom of the element to the top starting with black at the bottom which fades to transparency half way up the screen.

Just like that we have our text and the content of the component is finished!

Dynamic Text Using Container Queries

Something else occurred to me while I was writing this post. I wanted to include a scaled down version of this element in the article so the reader could see what I was building from the get go. This would require me to make the text size dynamic so that the text would not be too large in a smaller window. Conveniently, this gave me a reason to experiment with container queries for the first time.

For a long time in CSS, we have been able to size elements with respect to the viewport (browser window) size. Container queries allow you to size elements with respect to a parent element (a.k.a their container.)

The use of the container queries was quite simple. First you add the container-type property to the parent element and give it the value inline-size. From there the container at-rule is used with a media query-like syntax to change whatever CSS properties you like based on the size of the parent.

.mainImage {
	container-type: inline-size;
}

/* Class for the main text of the slideshow */
.title {
	font-size: 40px;
}

@container (width > 600px) {
    .title {
        font-size: 55px;
    }
}

@container (width > 1000px) {
    .title {
        font-size: 70px;
    }
}

Nifty!

A Final Touch

Since this screen saver is meant to display images over a whole screen, I’m going to assume that these images are going to be pretty large. With large image sizes they are going to make a moment to download over the internet and I want to avoid images slowly loading in while the screen is up.

To prevent this, I decided to preload images before they show up. Images can be programmatically preloaded by creating an image object with the URL.

const preloadImage = (src: string) => {
	return new Promise((resolve, reject) => {
		const image = new Image();
		image.onload = resolve;
		image.onerror = reject;
		image.src = src;
	})
}

When the component first loads, the preloadImage function is called for both the first image and the second image with a useEffect hook. To stop the animation from starting right away, I also added another useState to track when the images are loading in. When the loading is occurring, the animation-play-state CSS property is set to paused on the main image (and the text!) so that the animation does not start until the images are ready.

const NetflixScreenSaver = ({ screens }: { screens: string[] }) => {
	const [preloading, setPreloading] = useState(true);
	
	useEffect(() => {
		(async () => {
			const imagesToPreload = screens.slice(0, 2);
			await Promise.all(
				imagesToPreload.map((url) => preloadImage(url))
			);
			setPreloading(false);
		})();
	}, []);

	return (
		<div 
			className="mainImage" 
			style={{ animationPlayState: preloading ? 'paused' : 'running' }}
		>			
			...
		</div>
	);
}

This takes care of the first two images. Then every time an animation iteration completes, we can load the image after the next image so that everything is loaded into memory by the time that image is needed.

const handleAnimationIteration = () => {
	const imageAfterNext = (currentScreen + 2) % screens.length;
	preloadImage(screens[imageAfterNext]);
	// no need to await this and hold up everything, we can just
	// add this to the event loop and let it run when the browser
	// has time since our animation takes so long
}

With that the component avoids half loaded images stalling the animation.

Future Additions

I think I’ve got a pretty good component for now, but of course improvements could be made in the future.

A big improvement I am thinking about is being able to provide a function as a prop to the component to get images. This would help paginate the images so they do not all have to be provided up front. Providing images in this way also makes sense for this component since I assume the movies and TV shows shown to the user are algorithmically generated.

Conclusion

Overall, this component was a good excuse to learn a bit more about CSS animations and how to interact with them via JavaScript. I enjoyed the challenge of having to build something in the real world using web technologies and I hope to do something like this again soon. I welcome you to play around with the component a bit more. You can also take a look at the final source code if you’d like. Thank you for taking the time to read this article. Have a great day!