Transition Effect with CSS Masks

A tutorial on how to use CSS Masks to create some interesting looking slide transitions. Highly experimental!

Today we’d like to show you how to create an intriguingly simple, yet eye-catching transition effect using CSS Masks. Together with clipping, masking is another way of defining visibility and composite with an element. In the following tutorial we’ll show you how to apply the new properties for a modern transition effect on a simple slideshow. We’ll be applying animations utilizing the steps() timing function and move a mask PNG over an image to achieve an interesting transition effect.

Attention: Please keep in mind that this effect is highly experimental and only supported by some modern browsers (Chrome, Safari and Opera for now).

CSS Masks

Method of displaying part of an element, using a selected image as a mask

W3C Candidate Recommendation

Supported from the following versions:

Desktop

  • 120
  • 53
  • No
  • 106
  • 15

Mobile / Tablet

  • 15
  • 131
  • No
  • 131
  • 132

* denotes prefix required.

  • Supported:
  • Yes
  • No
  • Partially
  • Polyfill

Stats from caniuse.com

Keep in mind that Firefox has only partial support (it only supports inline SVG mask elements) so we’ll have a fallback for now. Hopefully, we can welcome support in all modern browsers very soon. Note that we’re adding Modernizr to check for support.

So let’s get started!

Creating the Mask Image

In this tutorial we’ll be going through the first example (demo 1).

For the mask transition effect to work, we will need an image that we’ll use to hide/show certain parts of our underlying image. That mask image will be a PNG with transparent parts on it. The PNG itself will be a sprite image and it looks as follows:

CSS Mask transition

While the black parts will show the current image, the white part (which is actually transparent) will be the masked part of our image that will reveal the second image.

In order to create the sprite image we will use this video. We import it into Adobe After Effects to reduce the timing of the video, remove the white part and export it as a PNG sequence.

To reduce the duration to 1.4 seconds (the time we want our transition to take) we’ll use the Time stretch effect.

CSS Mask transition

To remove the white part we will use Keying -> extract and set the white point to 0. In the screenshot below, the blue portion is the background of our composition, the transparent parts of the video.

CSS Mask transition

Finally, we can save our composition as a PNG sequence and then use Photoshop or a tool like this CSS sprite generator to generate a single image:

CSS Mask transition

This is one sprite image for a very organic looking reveal effect. We’ll create another, “inversed” sprite for the opposite kind of effect. You’ll find all the different sprites in the img folder of the demo files.

Now, that we’ve created the mask image, let’s dive into the HTML structure for our simple example slideshow.

The Markup

For our demo, we’ll create a simple slideshow to show the mask effect. Our slideshow will fill the entire screen and we’ll add some arrows that will trigger the slide transitions. The idea is to superpose the slides and then change the z-index of the incoming slide when the animation is over. The structure for our slideshow looks as follows:

<div class="page-view">
	<div class="project">
		<div class="text">
			<h1>“All good things are <br> wild & free”</h1>
			<p>Photo by Andreas Rønningen</p>
		</div>
	</div>
	<div class="project">
		<div class="text">
			<h1>“Into the wild”</h1>
			<p>Photo by John Price</p>
		</div>
	</div>
	<div class="project">
		<div class="text">
			<h1>“Is spring coming?”</h1>
			<p>Photo by Thomas Lefebvre</p>
		</div>
	</div>
	<div class="project">
		<div class="text">
			<h1>“Stay curious”</h1>
			<p>Photo by Maria</p>
		</div>
	</div>
	<nav class="arrows">
		<div class="arrow previous">
			<svg viewBox="208.3 352 4.2 6.4">
				<polygon class="st0" points="212.1,357.3 211.5,358 208.7,355.1 211.5,352.3 212.1,353 209.9,355.1"/>
			</svg>
		</div>
		<div class="arrow next">
			<svg viewBox="208.3 352 4.2 6.4">
				<polygon class="st0" points="212.1,357.3 211.5,358 208.7,355.1 211.5,352.3 212.1,353 209.9,355.1"/>
			</svg>
		</div>
	</nav>
</div>

The division page-view is our global container, it will contain all our slides. The project divisions are the slides of our slideshow; each one contains a title and a legend. Additionally, we’ll set an individual background image for each slide.

The arrows will serve as our trigger for the next or previous animation, and to navigate through the slides.

Let’s have a look at the style.

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

The CSS

In this part we’ll define the CSS for our effect.

We’ll set up the layout for a classic fullscreen slider, with some centered titles and the navigation at the bottom left of the page. Moreover, we’ll define some media queries to adapt the style for mobile devices.

Furthermore, we’ll set our sprite images as invisible backgrounds on our global container just so that we have them start loading when we open the page.

.demo-1 {
	background: url(../img/nature-sprite.png) no-repeat -9999px -9999px;
	background-size: 0;
}

.demo-1 .page-view {
	background: url(../img/nature-sprite-2.png) no-repeat -9999px -9999px;
	background-size: 0;
}

Each slide will have a different background-image:

.demo-1 .page-view .project:nth-child(1) {
	background-image: url(../img/nature-1.jpg);
}

.demo-1 .page-view .project:nth-child(2) {
	background-image: url(../img/nature-2.jpg);
}

.demo-1 .page-view .project:nth-child(3) {
	background-image: url(../img/nature-3.jpg);
}

.demo-1 .page-view .project:nth-child(4) {
	background-image: url(../img/nature-4.jpg);
}

This would of course be something that you’ll dynamically implement but we are interested in the effect, so let’s keep it simple.

We define a class named hide that we will add to the slide whenever we want to hide it. The class definition contains our sprite applied as a mask.

Knowing that a frame is 100% of the screen and our animation contains 23 images, we will need to set the width to 23 * 100% = 2300%.

Now we add our CSS animation utilizing steps. We want our sprite to stop at the beginning of our last frame. Hence, to achieve this, we need to count one less step than the total, which is 22 steps:

.demo-1 .page-view .project:nth-child(even).hide {
	-webkit-mask: url(../img/nature-sprite.png);
	mask: url(../img/nature-sprite.png);
	-webkit-mask-size: 2300% 100%;
	mask-size: 2300% 100%;
	-webkit-animation: mask-play 1.4s steps(22) forwards;
	animation: mask-play 1.4s steps(22) forwards;
}

.demo-1 .page-view .project:nth-child(odd).hide {
	-webkit-mask: url(../img/nature-sprite-2.png);
	mask: url(../img/nature-sprite-2.png);
	-webkit-mask-size: 7100% 100%;
	mask-size: 7100% 100%;
	-webkit-animation: mask-play 1.4s steps(70) forwards;
	animation: mask-play 1.4s steps(70) forwards;
}

Finally, we define the animation keyframes:

@-webkit-keyframes mask-play {
  from {
	-webkit-mask-position: 0% 0;
	mask-position: 0% 0;
  }
  to {
	-webkit-mask-position: 100% 0;
	mask-position: 100% 0;
  }
}

@keyframes mask-play {
  from {
	-webkit-mask-position: 0% 0;
	mask-position: 0% 0;
  }
  to {
	-webkit-mask-position: 100% 0;
	mask-position: 100% 0;
  }
}

And here we go; we now have a structured and styled slideshow. Let’s turn it into something functional!

CSS Mask transition

The JavaScript

We will be using zepto.js for this demo which is a very lightweight JavaScript framework similar to jQuery.

We start by declaring all our variables, setting the durations and the elements.

Then we initialize the events, get the current and the next slide, set the correct z-index.

function Slider() {
	// Durations
	this.durations = {
		auto: 5000,
		slide: 1400
	};
	// DOM
	this.dom = {
		wrapper: null,
		container: null,
		project: null,
		current: null,
		next: null,
		arrow: null
	};
	// Misc stuff
	this.length = 0;
	this.current = 0;
	this.next = 0;
	this.isAuto = true;
	this.working = false;
	this.dom.wrapper = $('.page-view');
	this.dom.project = this.dom.wrapper.find('.project');
	this.dom.arrow = this.dom.wrapper.find('.arrow');
	this.length = this.dom.project.length;
	this.init();
	this.events();
	this.auto = setInterval(this.updateNext.bind(this), this.durations.auto);
}
/**
 * Set initial z-indexes & get current project
 */
Slider.prototype.init = function () {
	this.dom.project.css('z-index', 10);
	this.dom.current = $(this.dom.project[this.current]);
	this.dom.next = $(this.dom.project[this.current + 1]);
	this.dom.current.css('z-index', 30);
	this.dom.next.css('z-index', 20);
};

We listen for the click event on the arrows, and if the slideshow is not currently involved in an animation, we check if the click was on the next or previous arrow. Like that we adapt the value of the “next” variable and we proceed to change the slide.

/**
 * Initialize events
 */
Slider.prototype.events = function () {
	var self = this;
	this.dom.arrow.on('click', function () {
		if (self.working)
			return;
		self.processBtn($(this));
	});
};
Slider.prototype.processBtn = function (btn) {
	if (this.isAuto) {
		this.isAuto = false;
		clearInterval(this.auto);
	}
	if (btn.hasClass('next'))
		this.updateNext();
	if (btn.hasClass('previous'))
		this.updatePrevious();
};
/**
 * Update next global index
 */
Slider.prototype.updateNext = function () {
	this.next = (this.current + 1) % this.length;
	this.process();
};
/**
 * Update next global index
 */
Slider.prototype.updatePrevious = function () {
	this.next--;
	if (this.next < 0)
		this.next = this.length - 1;
	this.process();
};

This function is the heart of our slideshow: we set the class “hide” to the current slide and once the animation is over, we reduce the z-index of the previous slide, increase the one of the current slide and then remove the hide class of the previous slide.

/**
 * Process, calculate and switch between slides
 */
Slider.prototype.process = function () {
	var self = this;
	this.working = true;
	this.dom.next = $(this.dom.project[this.next]);
	this.dom.current.css('z-index', 30);
	self.dom.next.css('z-index', 20);
	// Hide current
	this.dom.current.addClass('hide');
	setTimeout(function () {
		self.dom.current.css('z-index', 10);
		self.dom.next.css('z-index', 30);
		self.dom.current.removeClass('hide');
		self.dom.current = self.dom.next;
		self.current = self.next;
		self.working = false;
	}, this.durations.slide);
};

Adding the respective classes will trigger our animations which in turn apply the mask image to our slides. The main idea is to move the mask in a step animation function in order to create transition flow.

And that’s it! I hope you find this tutorial useful and have fun creating your own cool mask effects! Don’t hesitate to share your creations, I would love to see them!

Browser Support:
  • ChromeSupported
  • FirefoxNot supported
  • Internet ExplorerNot supported
  • SafariSupported
  • OperaSupported

References and Credits

Robin Delaporte

Creative technologist at Canva Sydney. CSS and animation lover, Robin is also a WebGL enthusiast.

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!

Feedback 35

Comments are closed.
  1. Nice effects and idea. On a 4k Monitor choppy even with a GTX1080 on Chrome, but very nice to watch on 1920×1040.

  2. Very nice effect to watch in the demo – good work. However, when I implement it the transition between slides is jerky even in chrome.

  3. Good to see this post, I am very much interested in css transition effects it makes website good look

  4. Hi! Can we use some of your code for our project. Our project is something like a page builder and we want to sell our projects when it is finished. Thank you and God bless!

  5. I am currently struggling in designing! because I am a developer and not much of a designer for a Professional Website Development Company. Thanks for the tips.

  6. Providing a way to control animation speed when changing CSS properties. Great!
    #convertbetter

  7. Awesome library! How would I change the CSS to make this slider a header for a one-page website? Is there anywhere I can style this as a fixed hero on a larger page?

  8. Nice tutorial, but its not very well explained how the transition mask itself is created, i understand the part how to do the animation. But creating the shape and animating seems to be missing

  9. Cool effect! Going to try to incorporate it into my next web design project. Thanks.

  10. How would you do the same effect but without the mask so the ink png animates and the next slide reveals?

  11. i tried it in my local host for a website, but the mask effect is not working. just images and text are changing. nothing else 🙁