Tilted Content Slideshow

A tutorial on how to recreate the slideshow seen on the FWA landing page with 3D effects involving random animations.

The FWA landing page has a really nice content slider that plays with 3D perspective on screenshots and animates them in an interesting way. Today we’d like to recreate part of that effect and make a simple content slideshow with some fancy 3D animations. The slideshow won’t be the same as the one on the FWA page: the items won’t be “floating” or moving on hover and we’ll only have a simple navigation.

If you have seen the effect over at the FWA landing page you will notice that the movement directions of the screenshots are random (moving up or down and sliding to the sides). We want to achieve the same effect by randomly adding some data-attributes that control the type of animation.

Please note that we’ll be using CSS 3D Transforms and CSS Animations which might not work in older or mobile browsers.

For the demo we are using some website screenshots from Zurb’s Responsive Gallery.

So, let’s get started!

The Markup

The slideshow has a main container with the class and ID “slideshow” and we can use an ordered list for our slides. Each list item contains a description with a title and a paragraph. It will also contain a division with the class “tiltview” where we will add our screenshots. The classes “col” an “row” will help us set the right layout for the inner anchors:

<div class="slideshow" id="slideshow">
	<ol class="slides">
		<li class="current">
			<div class="description">
				<h2>Some Title</h2>
				<p>Some description</p>
			</div>
			<div class="tiltview col">
				<a href="http://grovemade.com/"><img src="img/1_screen.jpg"/></a>
				<a href="https://tsovet.com/"><img src="img/2_screen.jpg"/></a>
			</div>
		</li>
		<li>
			<div class="description">
				<!-- ... -->
			</div>
			<div class="tiltview row">
				<!-- ... -->
			</div>
		</li>
		<li>
			<!-- ... -->
		</li>
	</ol>
</div>

We’ll also add a navigation element in our JavaScript which we’ll place right after the ordered list. It will consist of a nav with the right amount of spans.

Let’s already have a thought on how we will control the animations for each screenshot. In our script we set a data-attribute for a random incoming and outgoing animation. We’ll use the data-attributes data-effect-in and data-effect-out to control our animations in the CSS. We’ll check out the values for those attributes in a while. Let’s first check out the main style of the slideshow.

The CSS

Note that the CSS will not contain any vendor prefixes, but you will find them in the files (-webkit-).

Our slideshow wrapper and the ordered list will have the following style:

.slideshow {
	position: relative;
	margin-bottom: 100px;
}

.slides {
	list-style: none;
	padding: 0;
	margin: 0;
	position: relative;
	height: 500px;
	width: 100%;
	overflow: hidden;
	background: #ddd;
	color: #333;
}

The slideshow will be 500px high and we need to set the overflow to hidden, so that we don’t see the items fly out.
When we can’t build our slideshow because JavaScript is not enabled, we need to make sure that all slides are shown, so we set the height to auto:

.no-js .slides {
	height: auto;
}

Each list item will be positioned absolutely and occupy all available width and height. By default, we’ll set the visibility to hidden.
Each slide will also serve as the perspective container and we’ll define a perspective value of 1600px:

.slides > li {
	width: 100%;
	height: 100%;
	position: absolute;
	visibility: hidden;
	perspective: 1600px;
}

Let’s not forget the fallback:

.no-js .slides > li {
	position: relative;
	visibility: visible;
}

The navigation which is added dynamically, will appear as a set of lines. Each navigation item is a span and although we are using a tiny line, we want to make sure that the clickable area is actually bigger. This we can simulate by adding a white border:

.slideshow > nav {
	text-align: center;
	margin-top: 20px;
}

.slideshow > nav span {
	display: inline-block;
	width: 60px;
	height: 25px;
	border-top: 10px solid #fff;
	border-bottom: 10px solid #fff;
	background-color: #ddd;
	cursor: pointer;
	margin: 0 3px;
	transition: background-color 0.2s;
}

.slideshow > nav span:hover {
	background-color: #333;
}

.slideshow > nav span.current {
	background-color: #aaa;
}

The description will fill half of the width and since we want a transition on the opacity, we need to set it to 0 initially:

.description {
	width: 50%;
	padding: 2em 4em;
	font-size: 1.5em;
	position: relative;
	z-index: 1000;
	opacity: 0;
}

.no-js .description {
	opacity: 1;
}

.description h2 {
	font-size: 200%;
}

Now, let’s style the most crucial element in our slideshow. The division with the class “tiltview” will help us put our items into perspective. We need to add preserve-3d as transform style because some inner items will need to move on the Z-axis in some animations.
The “tiltview” wrapper will be centered by setting the top to 50% and transforming it -50% on the Y-axis. We’ll also rotate it on the X and Z-axis to create the 3D look:

.tiltview {
	position: absolute;
	left: 50%;
	width: 50%;
	top: 50%;
	transform-style: preserve-3d;
	transform: translateY(-50%) rotateX(60deg) rotateZ(35deg);
}

And the anchors and images will have the following style (the outline helps to avoid jagged edges in Firefox):

.tiltview a {
	outline: 1px solid transparent;
}

.tiltview a,
.tiltview a img {
	max-width: 100%;
	display: block;
	margin: 0 auto;
}

.tiltview a:first-child {
	margin-bottom: 30px;
}

For the row and column cases we’ll set the widths accordingly:

.tiltview.row a {
	width: 48%;
	width: calc(50% - 15px);
	margin: 0;
}

.tiltview.row a:nth-child(2) {
	left: 50%;
	left: calc(50% + 15px);
	position: absolute;
	top: 0;
}

In our script we will use the classes “show” and “hide” to control the visibility of the slides:

/* Show/Hide */
.slides > li.current,
.slides > li.show {
	visibility: visible;
}

The description will fade in and out:

.description {
	transition: opacity 0.75s;
}

.current .description,
.show .description {
	opacity: 1;
}

.hide .description {
	opacity: 0;
}

As we mentioned before, we’ll control the animations by using some data-attributes. We have to define two types of animations: the incoming one (when we show the next slide) and the outgoing one (when we hide the previous slide).
We want to be able to animate the items in all possible directions, so we’ll need six different types: move up, move down, slide up, slide down, slide left, slide right. This makes a total of 12 animations (incoming and outgoing).

So, let’s define the first one for moving the outgoing element up:

/***********************/
/* Move up */
/***********************/

.hide[data-effect-out="moveUpOut"] .tiltview a {
	animation: moveUpOut 1.5s both;
}

.hide[data-effect-out="moveUpOut"] .tiltview a:nth-child(2) {
	animation-delay: 0.25s;
}

@keyframes moveUpOut {
	25% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateZ(-30px);
	}
	100% {
		transform: translateZ(3000px);
	}
}

We define a slight animation delay for the second item and we use a custom cubic-bezier timing function to add some interesting momentum.

The second animation for this movement is the incoming one which has the initial and end step reversed:

.show[data-effect-in="moveUpIn"] .tiltview a {
	animation: moveUpIn 1.5s 0.5s both;
}

.show[data-effect-in="moveUpIn"] .tiltview a:nth-child(2) {
	animation-delay: 0.75s;
}

@keyframes moveUpIn {
	0% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateZ(-3000px);
	}
	75% {
		transform: translateZ(30px);
	}
	100% {
		transform: translateZ(0);
	}
}

As you might have noticed, we could simplify the animation delay for the “hide” and the “show” case, but keeping the two rules separated will allow for easier adaption in case you’d like to define some different delays.

The resting animations are as follows:

/***********************/
/* Move down */
/***********************/
.hide[data-effect-out="moveDownOut"] .tiltview a {
	animation: moveDownOut 1.5s both;
}

.hide[data-effect-out="moveDownOut"] .tiltview a:nth-child(2) {
	animation-delay: 0.25s;
}

@keyframes moveDownOut {
	25% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateZ(30px);
	}
	100% {
		transform: translateZ(-3000px);
	}
}

.show[data-effect-in="moveDownIn"] .tiltview a {
	animation: moveDownIn 1.5s 0.5s both;
}

.show[data-effect-in="moveDownIn"] .tiltview a:nth-child(2) {
	animation-delay: 0.75s;
}

@keyframes moveDownIn {
	0% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateZ(3000px);
	}
	75% {
		transform: translateZ(-30px);
	}
	100% {
		transform: translateZ(0);
	}
}

/***********************/
/* Slide up */
/***********************/
.hide[data-effect-out="slideUpOut"] .tiltview a {
	animation: slideUpOut 1.5s both;
}

.hide[data-effect-out="slideUpOut"] .tiltview a:nth-child(2) {
	animation-delay: 0.25s;
}

@keyframes slideUpOut {
	25% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateY(30px);
	}
	100% {
		transform: translateY(-3000px);
	}
}

.show[data-effect-in="slideUpIn"] .tiltview a {
	animation: slideUpIn 1.5s 0.5s both;
}

.show[data-effect-in="slideUpIn"] .tiltview a:nth-child(2) {
	animation-delay: 0.75s;
}

@keyframes slideUpIn {
	0% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateY(3000px);
	}
	75% {
		transform: translateY(-30px);
	}
	100% {
		transform: translateY(0);
	}
}

/***********************/
/* Slide down */
/***********************/
.hide[data-effect-out="slideDownOut"] .tiltview a {
	animation: slideDownOut 1.5s both;
}

.hide[data-effect-out="slideDownOut"] .tiltview.row a:nth-child(2),
.hide[data-effect-out="slideDownOut"] .tiltview.col a:first-child {
	animation-delay: 0.25s;
}

@keyframes slideDownOut {
	25% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateY(-30px);
	}
	100% {
		transform: translateY(3000px);
	}
}

.show[data-effect-in="slideDownIn"] .tiltview a {
	animation: slideDownIn 1.5s 0.5s both;
}

.show[data-effect-in="slideDownIn"] .tiltview.row a:nth-child(2),
.show[data-effect-in="slideDownIn"] .tiltview.col a:first-child {
	animation-delay: 0.75s;
}

@keyframes slideDownIn {
	0% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateY(-3000px);
	}
	75% {
		transform: translateY(30px);
	}
	100% {
		transform: translateY(0);
	}
}

/***********************/
/* Slide left */
/***********************/
.hide[data-effect-out="slideLeftOut"] .tiltview a {
	animation: slideLeftOut 1.5s both;
}

.hide[data-effect-out="slideLeftOut"] .tiltview a:nth-child(2) {
	animation-delay: 0.25s;
}

@keyframes slideLeftOut {
	25% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateX(30px);
	}
	100% {
		transform: translateX(-5000px);
	}
}

.show[data-effect-in="slideLeftIn"] .tiltview a {
	animation: slideLeftIn 1.5s 0.5s both;
}

.show[data-effect-in="slideLeftIn"] .tiltview a:nth-child(2) {
	animation-delay: 0.75s;
}

@keyframes slideLeftIn {
	0% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateX(3000px);
	}
	75% {
		transform: translateX(-30px);
	}
	100% {
		transform: translateX(0);
	}
}

/***********************/
/* Slide right */
/***********************/
.hide[data-effect-out="slideRightOut"] .tiltview a {
	animation: slideRightOut 1.5s both;
}

.hide[data-effect-out="slideRightOut"] .tiltview.col a:nth-child(2),
.hide[data-effect-out="slideRightOut"] .tiltview.row a:first-child {
	animation-delay: 0.25s;
}

@keyframes slideRightOut {
	25% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateX(-30px);
	}
	100% {
		transform: translateX(3000px);
	}
}

.show[data-effect-in="slideRightIn"] .tiltview a {
	animation: slideRightIn 1.5s 0.5s both;
}

.show[data-effect-in="slideRightIn"] .tiltview.col a:nth-child(2),
.show[data-effect-in="slideRightIn"] .tiltview.row a:first-child {
	animation-delay: 0.75s;
}

@keyframes slideRightIn {
	0% {
		animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
		transform: translateX(-5000px);
	}
	75% {
		transform: translateX(30px);
	}
	100% {
		transform: translateX(0);
	}
}

Note that for some animations we need to define the animation delay for the first child instead of the second. We need to do this so that the anchors don’t overlap each other for some directions.

When we don’t have support for CSS 3D Transforms or transform-style: preserve-3d, then we want to provide a simple fallback:

/* Fallback for no 3D Transforms and no preserve-3d */
.no-csstransformspreserve3d .show .tiltview a,
.no-csstransformspreserve3d .hide .tiltview a,
.no-csstransforms3d .show .tiltview a,
.no-csstransforms3d .hide .tiltview a {
	animation: none !important;
}

.no-csstransforms3d .tiltview.col {
	top: -50%;
}

.no-csstransforms3d .tiltview.row {
	top: 20px;
}

And last, but not least, we need to make sure that we have a reasonable look for smaller screens. In this case we want the anchors with their screenshots to be just decoration, so we’ll set their opacity lower and make them be not clickable:

@media screen and (max-width: 65.3125em) {
	.description,
	.tiltview {
		width: 100%;
	}

	.tiltview {
		left: 0;
		opacity: 0.3;
		pointer-events: none;
	}
}

@media screen and (max-width: 33.75em) {
	.description {
		font-size: 1.1em;
	}

	.slideshow > nav span {
		width: 20px;
		height: 40px;
		margin: 0 10px;
	}
}

@media screen and (max-width: 24em) {
	.slides {
		height: 320px;
	}

	.description {
		font-size: 1em;
		padding: 1.4em;
	}

	.no-csstransforms3d .tiltview.col,
	.no-csstransforms3d .tiltview.row {
		top: 0;
	}
}

And that’s the style! Let’s do our slideshow script.

The JavaScript

We will start by initializing some variables, like the two arrays with the animation class names that control the incoming and outgoing of our items. These will be picked randomly and set to the items when we navigate the slideshow. Other variables are the items, the current value of the selected item (we assume this will be the first one) and the total number of items inside the slider:

function TiltSlider( el, options ) {
	this.el = el;
	// available effects for the animations (animation class names) - when an item comes in or goes out
	this.animEffectsOut = ['moveUpOut','moveDownOut','slideUpOut','slideDownOut','slideLeftOut','slideRightOut'];
	this.animEffectsIn = ['moveUpIn','moveDownIn','slideUpIn','slideDownIn','slideLeftIn','slideRightIn'];
	// the items
	this.items = this.el.querySelector( 'ol.slides' ).children;
	// total number of items
	this.itemsCount = this.items.length;
	if( !this.itemsCount ) return;
	// index of the current item
	this.current = 0;
	this.options = extend( {}, this.options );
	extend( this.options, options );
	this._init();
}

In order to navigate the slideshow we will add some navigation spans that, when clicked, will make the respective slideshow item appear. The total number of spans will be the same as the total number of items. Let’s add the navigation to our component:

TiltSlider.prototype._addNavigation = function() {
	// add nav "dots"
	this.nav = document.createElement( 'nav' )
	var inner = '';
	for( var i = 0; i < this.itemsCount; ++i ) {
		inner += i === 0 ? '' : '';
	}
	this.nav.innerHTML = inner;
	this.el.appendChild( this.nav );
	this.navDots = [].slice.call( this.nav.children );
}

Next, we need to bind the onclick event to the navigation spans. If we click on any other but the current span, then the current item should animate out and the new one should animate in.

TiltSlider.prototype._initEvents = function() {
	var self = this;
	// show a new item when clicking the navigation "dots"
	this.navDots.forEach( function( dot, idx ) {
		dot.addEventListener( 'click', function() {
			if( idx !== self.current ) {
				self._showItem( idx );
			}
		} );
	} );
}

We need to reference and work with the current item and also the next one that has to appear. We will add and remove classes from both of them in order to apply the respective animations. The animation (i.e. the data-attribute value) itself will be randomly picked from our animEffectsOut and animEffectsIn arrays as described before.

TiltSlider.prototype._showItem = function( pos ) {
	if( this.isAnimating ) {
		return false;
	}
	this.isAnimating = true;

	classie.removeClass( this.navDots[ this.current ], 'current' );

	var self = this,
		// the current item
		currentItem = this.items[ this.current ];

	this.current = pos;

	// next item to come in
	var nextItem = this.items[ this.current ],
		// set random effects for the items
		outEffect = this.animEffectsOut[ Math.floor( Math.random() * this.animEffectsOut.length ) ],
		inEffect = this.animEffectsIn[ Math.floor( Math.random() * this.animEffectsOut.length ) ];

	currentItem.setAttribute( 'data-effect-out', outEffect );
	nextItem.setAttribute( 'data-effect-in', inEffect );

	classie.addClass( this.navDots[ this.current ], 'current' );

	var cntAnims = 0,
		// the number of elements that actually animate inside the current item
		animElemsCurrentCount = currentItem.querySelector( '.tiltview' ).children.length, 
		// the number of elements that actually animate inside the next item
		animElemsNextCount = nextItem.querySelector( '.tiltview' ).children.length,
		// keep track of the number of animations that are terminated
		animEndCurrentCnt = 0, animEndNextCnt = 0,
		// check function for the end of each animation
		isFinished = function() {
			++cntAnims;
			if( cntAnims === 2 ) {
				self.isAnimating = false;
			}
		},
		// function for the end of the current item animation
		onEndAnimationCurrentItem = function() {
			++animEndCurrentCnt;
			var endFn = function() {
				classie.removeClass( currentItem, 'hide' );
				classie.removeClass( currentItem, 'current' );
				isFinished();
			};

			if( !isSupported ) {
				endFn();
			}
			else if( animEndCurrentCnt === animElemsCurrentCount ) {
				currentItem.removeEventListener( animEndEventName, onEndAnimationCurrentItem );
				endFn();
			}
		},
		// function for the end of the next item animation
		onEndAnimationNextItem = function() {
			++animEndNextCnt;
			var endFn = function() {
				classie.removeClass( nextItem, 'show' );
				classie.addClass( nextItem, 'current' );
				isFinished();
			};

			if( !isSupported ) {
				endFn();
			}
			else if( animEndNextCnt === animElemsNextCount ) {
				nextItem.removeEventListener( animEndEventName, onEndAnimationNextItem );
				endFn();
			}
		};

	if( isSupported ) {
		currentItem.addEventListener( animEndEventName, onEndAnimationCurrentItem );
		nextItem.addEventListener( animEndEventName, onEndAnimationNextItem );
	}
	else {
		onEndAnimationCurrentItem();
		onEndAnimationNextItem();
	}

	classie.addClass( currentItem, 'hide' );
	classie.addClass( nextItem, 'show' );
}

And that’s it! We hope you enjoyed this tutorial and find it useful!

Note that IE11 still does not support transform-style: preserve-3d, so the fallback will be shown there.

Tagged with:

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 up to date with the latest web design and development news and relevant updates from Codrops.