How to Create a Tiled Background Slideshow

A tutorial that shows how to recreate the four tiles slideshow effect seen on the website of Serge Thoroval. Using 3D transforms, transitions and animations, the aim is to implement a smooth effect and add some variations.




Today we’d like to show you how to recreate the background image slideshow seen on the stunning website of Serge Thoroval’s Atelier that was coded and designed by talented Jean-Christophe Suzanne and Jonathan Da Costa. If you haven’t seen the website, you should definitely go and check it out; it’s full of interesting and creative effects. The slideshow that we are recreating today, is made up of four tiles that move individually, making the image split while scaling up the new image. This combines into a really nice effect which we will re-implement today using 3D transforms, transitions and animations. In addition to that effect we’ll also add two more variations. The aim that we want to keep in mind, is to achieve a super-smooth effect experience.

Note that we’ll be using very modern CSS properties, so the desired effect will only work in the newest browser versions.

The images used in the demos are made with really great mobile phone PSD mockups from the team of Mockuuups.com. You can find the free iPhone 5S Mockups in this Dribbble post by Tomas Jasovsky.

Let’s get started with the markup.

The Markup

For the slideshow we will need a special structure that will allow us to play with four different tiles, all containing the same image but with different positioning. We need to make sure that everything stretches over the full viewport since this is a “fullscreen” slideshow. We’ll define a simple initial structure that will allow us to specify which images will be visible in each panel (or slide) and then we’ll create our repeated structure for the tiles. So, initially we want something like this:

<div id="boxgallery" class="boxgallery" data-effect="effect-1">
	<div class="panel"><img src="img/1.jpg" alt="Image 1"/></div>
	<div class="panel"><img src="img/2.jpg" alt="Image 2"/></div>
	<div class="panel"><img src="img/3.jpg" alt="Image 3"/></div>
	<div class="panel"><img src="img/4.jpg" alt="Image 4"/></div>
</div>

In order to be able to animate the image by breaking it into tiles we will need the following repeated structure for each panel:

<div id="boxgallery" class="boxgallery" data-effect="effect-1">
	<div class="panel current">
		<div class="bg-tile">
			<div class="bg-img"><img src="img/1.jpg" /></div>
		</div>
		<div class="bg-tile">
			<div class="bg-img"><img src="img/1.jpg" /></div>
		</div>
		<div class="bg-tile">
			<div class="bg-img"><img src="img/1.jpg" /></div>
		</div>
		<div class="bg-tile">
			<div class="bg-img"><img src="img/1.jpg" /></div>
		</div>
	</div>
	<div class="panel">
		<div class="bg-tile">
			<div class="bg-img"><img src="img/2.jpg" /></div>
		</div>
		<div class="bg-tile">
			<div class="bg-img"><img src="img/2.jpg" /></div>
		</div>
		<div class="bg-tile">
			<div class="bg-img"><img src="img/2.jpg" /></div>
		</div>
		<div class="bg-tile">
			<div class="bg-img"><img src="img/2.jpg" /></div>
		</div>
	</div>
	<div class="panel">
		<!-- ... -->
	</div>
	<div class="panel">
		<!-- ... -->
	</div>
	<nav>
		<span class="prev"><i></i></span>
		<span class="next"><i></i></span>
	</nav>
</div>

We’ll also have to add a navigation so that we can browse through the panels. The data attribute data-effect will make it possible to define some individual variations of the effect.

Naturally, you might look at the structure and think, why not use background images instead of image elements? After some tries on what performs better cross-browser wise, we came to the conclusion that using a background image together with background-size can cause quite choppy transitions. Using for example, background-size: cover can be a major source of jerkiness. Another critical thing that causes twitchy transitions (at least in Chrome) is using percentage values when translating. This might not be noticeable at first glance but when comparing with a pixel-based translation, you will be able to see an impactful difference. For that reason we will not be using classes to control the transforms, but we’ll instead do it in our script by getting the pixel values of the viewport and applying the respective translation values to the tiles’ “background images”.

Let’s add some style to this thing.

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

With the generated structure in mind, we’ll now add the style.
Note that the CSS will not contain any vendor prefixes, but you will find them in the files.

First off, we are in the “fullscreen” realm, so we need to prepare our page elements for that. Setting the html, the body and the main container to a height of 100% will allow us to expand our slideshow to the full height of the viewport:

html, body, .container {
	height: 100%;
}

The main wrapper and the child divisions will all be of absolute positioning and the panels will occupy all the width and height:

.js .boxgallery,
.js .boxgallery div {
	position: absolute;
}

.js .boxgallery,
.js .panel {
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
}

Since our effects will potentially have elements moving out of their limits, we’ll make sure that nothing overflows:

.js .boxgallery,
.bg-tile,
.bg-img {
	overflow: hidden;
}

When looking at the effect of Serge Thoroval’s slideshow, you’ll notice that when navigating, the newly revealed slide scales up. We’ll be using 3D transforms wherever we can so we’ll need to add some perspective to the slide wrapper so that we can then translate the individual tiles on the z-Axis, i.e. make them scale down visually:

.panel {
	z-index: 0;
	perspective: 1200px;
}

Next, we have to style the tiles. Since we have four tiles, each tile will be 50% high and wide:

.bg-tile {
	width: 50%;
	height: 50%;
}

Let’s position each tile:

.bg-tile:nth-child(2),
.bg-tile:nth-child(4) {
	left: 50%;
}

.bg-tile:nth-child(3),
.bg-tile:nth-child(4) {
	top: 50%;
}

The inner division with the class bg-img will occupy the whole tile:

.bg-img {
	width: 100%;
	height: 100%;
	background: #999;
}

In order to place the images in a way that only shows one complete image per tile, we need to set it’s width and height to the double. Using percentages on both, width and height will distort our image so we need to make sure that we maintain the aspect ration. We can do that by setting only one of the dimensions to 200% and let the other one be defined automatically. But which one can we set? Well, that all depends on whether our viewport is more wide than high and vice versa. Fortunately, we have media queries and one expression in particular will help us do exactly what we want: min-aspect-ratio. This allows us to simply take the real size of our images as ratio value (width/height) and define, when we want the width and when the height to be calculated automatically:

.bg-img img {
	position: absolute;
	display: block;
	height: 200%;
}

@media screen and (min-aspect-ratio: 1280/850) {
	.bg-img img {
		width: 200%;
		height: auto;
	}
}

You can fine-tune this method by also adding width/height expressions in order to offer the desired viewing experience of the image for any screensize. For out slideshow, this little simple method will do.

Each image needs to be positioned as well:

.bg-tile:nth-child(2) .bg-img img,
.bg-tile:nth-child(4) .bg-img img {
	left: -100%;
}

.bg-tile:nth-child(3) .bg-img img,
.bg-tile:nth-child(4) .bg-img img {
	top: -100%;
}

The navigational arrows have the “Fill Path” style from the Arrow Navigation Styles.

So let’s define what happens when we navigate.
First of all, we need to set a z-index value to the current panel so that it stays on top of all the others. Since we’ve previously set all out panels’ z-indices to 0, we can use a value like 1. But because we’ll have a current panel but also one that needs to be shown directly under it (the next or previous one), we’ll set the z-indices as follows:

.panel.current {
	z-index: 2;
}

.panel.active {
	z-index: 1;
}

This will guarantee that the current one is on top of any other and the newly coming one is directly under it.

Considering that we will add the individual 3D transforms to each tile’s bg-img in our JavaScript (remember, we want to use pixel-based values for which we need the window width and height), we still want to set some things in our style sheet, like the transition itself and the animation for the new panel.
So, let’s define a transition on the transform property with a time and easing function that fit nicely:

.panel.current .bg-img {
	transition: transform 1.1s ease-in-out;
}

For the first effect (the one that we are trying to recreate) and the second one (which moves the tiles to the outside), we want the following panel to appear with a scale down effect:

.boxgallery[data-effect="effect-1"] .panel.active .bg-tile,
.boxgallery[data-effect="effect-2"] .panel.active .bg-tile {
	animation: scaleDown 1.1s ease-in-out;
}

@keyframes scaleDown {
	from { transform: translate3d(0,0,380px); }
	to { transform: translate3d(0,0,0); }
}

The following variation of the effect looks really great when adding a little delay to each tile’s child and making everything a bit faster with a nice cubic-bezier timing function. This variation will make the tiles move to the sides when navigating next and up/down when navigating to the previous panel:

/* Variation 2 */

.boxgallery[data-effect="effect-2"] .panel.current .bg-img {
	transition: transform 0.9s cubic-bezier(0.7,0,0.3,1);
}

.boxgallery[data-effect="effect-2"] .panel.current .bg-tile:nth-child(2) .bg-img {
	transition-delay: 0.15s;
}

.boxgallery[data-effect="effect-2"] .panel.current .bg-tile:nth-child(3) .bg-img {
	transition-delay: 0.3s;
}

.boxgallery[data-effect="effect-2"] .panel.current .bg-tile:nth-child(4) .bg-img {
	transition-delay: 0.45s;
}

The third variation serves as an example on how to use an overlay that will fade out once a new panel is revealed:

/* Variation 3 */

.boxgallery[data-effect="effect-3"] .panel::after {
	position: absolute;
	width: 100%;
	height: 100%;
	background: rgba(0,0,0,0.8);
	content: '';
	transition: opacity 1.1s ease-in-out;
}

.boxgallery[data-effect="effect-3"] .panel.current::after,
.boxgallery[data-effect="effect-3"] .panel.active::after {
	opacity: 0;
}

.boxgallery[data-effect="effect-3"] .panel.current::after {
	transition: none;
}

We’ll also use the same cubic-bezier timing function and define some delays (same as before).

.boxgallery[data-effect="effect-3"] .panel.current .bg-img {
	transition: transform 1.1s cubic-bezier(0.7,0,0.3,1);
}

.boxgallery[data-effect="effect-3"] .panel.current .bg-tile:nth-child(2) .bg-img {
	transition-delay: 0.15s;
}

.boxgallery[data-effect="effect-3"] .panel.current .bg-tile:nth-child(3) .bg-img {
	transition-delay: 0.3s;
}

.boxgallery[data-effect="effect-3"] .panel.current .bg-tile:nth-child(4) .bg-img {
	transition-delay: 0.45s;
}

After we go through the JavaScript, make sure to play with these values and add a new effect, there are so many possibilities!

The JavaScript

Like mentioned before, we’ve opted to manipulate the translation values directly in the JS, using pixel-based values obtained from the viewport width and height for a smoother transition. Alternatively, we could set classes and define everything in the stylesheet using percentage-based values, but this leaves us with jerky transitions. (In case we would use an effect that does not need any of those viewport dependent values (e.g. opacity, absolute values, scale, etc.) we could definitely go with the class-based solution.)

Let’s have a look at the init function.
We will start by defining the transforms that we want to apply to the panels as we navigate the slideshow. We need to specify which transforms we want the panels to have for both directions (navigate to the right and left, next and previous, respectively). Let’s define a fitting structure which we set in the setTransforms function.

Next, we get the effect type from the data-effect attribute. This will indicate which transforms we pick from the previously created structure.
We then initialize and cache some variables and finally we create the necessary DOM structure for our four boxes. For that, the image in each panel is replaced by four divisions, each one having an image element with the same src like the image. We also add the nav element at this point.

BoxesFx.prototype._init = function() {
	// set transforms configuration
	this._setTransforms();
	// which effect
	this.effect = this.el.getAttribute( 'data-effect' ) || 'effect-1';
	// check if animating
	this.isAnimating = false;
	// the panels
	this.panels = [].slice.call( this.el.querySelectorAll( '.panel' ) );
	// total number of panels (4 for this demo)
	//this.panelsCount = this.panels.length;
	this.panelsCount = 4;
	// current panel´s index
	this.current = 0;
	classie.add( this.panels[0], 'current' );
	// replace image with 4 divs, each including the image
	var self = this;
	this.panels.forEach( function( panel ) {
		var img = panel.querySelector( 'img' ), imgReplacement = '';
		for( var i = 0; i < self.panelsCount; ++i ) {
			imgReplacement += '<div class="bg-tile"><div class="bg-img"><img src="' + img.src + '" /></div></div>'
		}
		panel.removeChild( img );
		panel.innerHTML = imgReplacement + panel.innerHTML;
	} );
	// add navigation element
	this.nav = document.createElement( 'nav' );
	this.nav.innerHTML = '<span class="prev"><i></i></span><span class="next"><i></i></span>';
	this.el.appendChild( this.nav );
	// initialize events
	this._initEvents();
}

The translate values are calculated using the window width and height. We also add 10 pixels so that the transitions don’t really end in the last visible point, but rather a bit further, adding some more smoothness to the effects:

BoxesFx.prototype._setTransforms = function() {
	this.transforms = {
		'effect-1' : {
			'next' : [
				'translate3d(0, ' + (win.height/2+10) + 'px, 0)', // transforms for panel 1
				'translate3d(-' + (win.width/2+10) + 'px, 0, 0)', // transforms for panel 2
				'translate3d(' + (win.width/2+10) + 'px, 0, 0)', // transforms for panel 3
				'translate3d(0, -' + (win.height/2+10) + 'px, 0)' // transforms for panel 4
			],
			'prev' : [
				'translate3d(' + (win.width/2+10) + 'px, 0, 0)',
				'translate3d(0, ' + (win.height/2+10) + 'px, 0)',
				'translate3d(0, -' + (win.height/2+10) + 'px, 0)',
				'translate3d(-' + (win.width/2+10) + 'px, 0, 0)'
			]
		},
		'effect-2' : {
			'next' : [
				'translate3d(-' + (win.width/2+10) + 'px, 0, 0)',
				'translate3d(' + (win.width/2+10) + 'px, 0, 0)',
				'translate3d(-' + (win.width/2+10) + 'px, 0, 0)',
				'translate3d(' + (win.width/2+10) + 'px, 0, 0)'
			],
			'prev' : [
				'translate3d(0,-' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,-' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,' + (win.height/2+10) + 'px, 0)'
			]
		},
		'effect-3' : {
			'next' : [
				'translate3d(0,' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,' + (win.height/2+10) + 'px, 0)'
			],
			'prev' : [
				'translate3d(0,-' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,-' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,-' + (win.height/2+10) + 'px, 0)',
				'translate3d(0,-' + (win.height/2+10) + 'px, 0)'
			]
		}
	};	
}

Next thing to do is to initialize the events on the navigation arrows, and the window resize event (we need to reset our transforms):

BoxesFx.prototype._initEvents = function() {
	var self = this, navctrls = this.nav.children;
	// previous action
	navctrls[0].addEventListener( 'click', function() { self._navigate('prev') } );
	// next action
	navctrls[1].addEventListener( 'click', function() { self._navigate('next') } );
	// window resize
	window.addEventListener( 'resize', function() { self._resizeHandler(); } );
}

Let’s take a look at the navigate function.
First, we get both, the current and the “upcoming” panels. We also need to reset the current variable value depending on the navigation action we take.
Next, we add the active class to the current panel in order to trigger any animation or transition the element might have (as previously defined in our style sheet). We finally set the transforms to the current panel which we defined before. This should make the four panels animate and cause the next slide to reveal. We just need to make sure we reset both, the classes we’ve added and the transforms on the elements. We do that once all the transitions for all four panels have finished:

BoxesFx.prototype._navigate = function( dir ) {
	if( this.isAnimating ) return false;
	this.isAnimating = true;

	var self = this, currentPanel = this.panels[ this.current ];

	if( dir === 'next' ) {
		this.current = this.current < this.panelsCount - 1 ? this.current + 1 : 0;			
	}
	else {
		this.current = this.current > 0 ? this.current - 1 : this.panelsCount - 1;
	}

	// next panel to be shown
	var nextPanel = this.panels[ this.current ];
	// add class active to the next panel to trigger its animation
	classie.add( nextPanel, 'active' );
	// apply the transforms to the current panel
	this._applyTransforms( currentPanel, dir );

	// let´s track the number of transitions ended per panel
	var cntTransTotal = 0,
		
		// transition end event function
		onEndTransitionFn = function( ev ) {
			if( ev && !classie.has( ev.target, 'bg-img' ) ) return false;

			// return if not all panel transitions ended
			++cntTransTotal;
			if( cntTransTotal < self.panelsCount ) return false;

			if( support.transitions ) {
				this.removeEventListener( transEndEventName, onEndTransitionFn );
			}

			// remove current class from current panel and add it to the next one
			classie.remove( currentPanel, 'current' );
			classie.add( nextPanel, 'current' );
			// reset transforms for the currentPanel
			self._resetTransforms( currentPanel );
			// remove class active
			classie.remove( nextPanel, 'active' );
			self.isAnimating = false;
		};

	if( support.transitions ) {
		currentPanel.addEventListener( transEndEventName, onEndTransitionFn );
	}
	else {
		onEndTransitionFn();
	}
}

BoxesFx.prototype._applyTransforms = function( panel, dir ) {
	var self = this;
	[].slice.call( panel.querySelectorAll( 'div.bg-img' ) ).forEach( function( tile, pos ) {
		tile.style.WebkitTransform = self.transforms[self.effect][dir][pos];
		tile.style.transform = self.transforms[self.effect][dir][pos];
	} );
}

BoxesFx.prototype._resetTransforms = function( panel ) {
	[].slice.call( panel.querySelectorAll( 'div.bg-img' ) ).forEach( function( tile ) {
		tile.style.WebkitTransform = 'none';
		tile.style.transform = 'none';
	} );
}

And that's it! I hope you enjoyed this tutorial and find it useful!

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

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

👾 Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!

Feedback 86

Comments are closed.
    • Well me neither for that matter…
      You simply cannot stop being that awesome, can you? Great job!

    • OMG ML! You are just….just…..{No Word to Express}

      Thank you so much for sharing your talented mind with this community!

  1. I’m always looking forward to your latest posts, this one is also amazing and very inspiring, thank you!

  2. I love this. Would love to use more than 4 photos for the slideshow. Any ideas? TIA

  3. Very impressive animation! I also love the way you use raw Javascript, instead of jQuery!

  4. Super cool! Why not post your tutorials in a codepen or jsfiddle link? I’d argue that 9/10 users prefer this to the download method.

    • If you are referring to being able to edit and play with the code, then stay tuned! We will be adding that feature in the near future 😉 Cheers

  5. How can we change to put different headers for each panel (change in .js)?
    Great tutorial!
    Thanks!

  6. Great work. Any way to add an automatic transition to this, as opposed to using the nav arrows? And any way to add rotating comments to each slide?

  7. Amazing work, thank you. If I want to add more images instead of four. Which part of the code I need to modify.

  8. Amazing work, thank you.
    but Any way to add an automatic transition to this 😀

  9. This is fantastic, but I am surprised to see no support for retina displays! Will this be forthcoming? Would be nice.

  10. More than four photos? I did it by adding some lines to boxesFx.js.

    In the section about 1/3 down with transforms for 1 panel, copy/paste the line a few times for effect-1, effect-2, effect-3 in the prev/next.. All up, 8 lines for each effect rather than the original 4.

    A bit further up, I changed this.panelsCount = 4; to 8.

    • I didn’t get it. Can you please explain a little. i tried as you said but it just didn’t work!! i’m new to coding. 🙁 🙁

  11. The Slides Are Being Triggered using the arrow keys how can we make it change automatically after some seconds.????

  12. Very nice technique. I am constantly amazing by the use of background features. I think it really does add something special to your website.

  13. UPDATE Re: retina, it actually works with retina.js (retinajs.com) so it’s a doddle 🙂

  14. Hey this is a great tutorial but I was wondering did anyone know where to tweak the code so that the slider wasn’t always on top? I want to it to be the second div on the homepage.

  15. If you want on every slide a different text or element here is a method

    Make sure you give all the elements that you want to change “container0” and in the next panel “container1”
    html:
    <div class="panel"> <header class="container0"> <h1>Heading</h1> </header> <p class="container0"> A paragraph </p> <img src="img/1.jpg" alt="Image 1" /> </div> <div class="panel"> <header class="container1"> <h1>Heading</h1> </header> <p class="container1"> A paragraph </p> <img src="img/2.jpg" alt="Image 2" /> </div> < -- Panel -- > ... < -- Panel -- > ...

    you have to replace the following javascript code and you need jquery

    javascript:
    // goto next or previous slide BoxesFx.prototype._navigate = function (dir) { if (this.isAnimating) return false; this.isAnimating = true; var self = this, currentPanel = this.panels[this.current]; var position = 0; if (dir === 'next') { this.current = this.current < this.panelsCount - 1 ? this.current + 1 : 0; position = this.current == 0 ? this.panelsCount - 1 : this.current - 1; } else { this.current = this.current > 0 ? this.current - 1 : this.panelsCount - 1; position = this.current == this.panelsCount - 1 ? 0 : this.current + 1; } // next panel to be shown var nextPanel = this.panels[this.current]; // add class active to the next panel to trigger its animation classie.add(nextPanel, 'active'); // Fade for containers var elems = document.getElementsByClassName('container' + position); for (var i = 0; i < elems.length; i++) { $(elems[i]).fadeOut("slow"); } elems = document.getElementsByClassName('container' + this.current); for (var i = 0; i < elems.length; i++) { $(elems[i]).fadeIn("slow"); }

  16. Thanks for nice tutorials. This is really what i exactly need. Very helpful for complete my project.

  17. Hi MaryLou,
    great slideshow!
    I have an issue on Android tablets: the slideshow stop working after clicking the left or right arrow the first time.
    Is there a workaround ?
    The slideshow works OK on iPAd but I need to implement it also on Android tablets (I have a Samsung Galaxy Tab)

    In addition, is there a way to customize arrows and chenge them into a PNG image ?

    Thanks for help on this.

    Frank

  18. Thank you Mary Lou for this amazing tutorial.
    A question : Is it possible to put the slideshow in auto play ?

    Thank you

  19. //FUNCTION AUTO-CLICK
    var interval = null;
    $(function(){
    interval = setInterval(callFunc, 5000);
    });
    function callFunc(){
    $(‘.next’).trigger(‘click’);
    }

    • Hi, thanks for the code. Where do you put this code exactly for it to autoplay? Any help is greatly appreciated. 🙂

    • You give you the code to add classie.js file. worked thanks
      tiled slider autoplay yukar?daki kodu classie.js dosyas?n?n sonuna ekledi?imizde çal??t?…

  20. Thank you Mary Lou for this great code.
    I have notice that many comments talk about an autoplay option. Can you help us with the code?

  21. Hello,

    It’s possible to display the slideshow not in fullscreen, if yes, how can I use it not in fullscreen please?

    Thanks for your great jobs.

    Léo

  22. wow that was cool, very beautiful.
    It worked fine on my Mozilla But wasn’t working in other browsers like IE. I don’t know why. Any help pls

  23. It’s an amazing tutorial, thank you! I loved the hover state on the previous and next buttons. Do you have any reference on how this is done? I mostly get the idea, but would be interesting to know all the details. Thanks!

  24. Hello, i think many people have the same problem with the project. Is really cool, but how can i change the quantity of images of the background? I mean the BACKGROUND, not the divisions of the transitions. Someone can help us? Thanks

  25. Hi! Great tutorial, but how can I add more than 4 slide? Can you help me please?