JavaScript slideshows: an introduction

JavaScript slideshows: an introduction

In this article we'll cover the basics of JavaScript slideshows.

In this article we'll cover the basics of JavaScript slideshows. We're going to describe the fundamental building blocks of a JavaScript slideshow (HTML, CSS, JavaScript) and the basic techniques used to create JavaScript slideshows. We need to focus on the bases in order to develop more complex and advanced slideshows.

The JavaScript code will be provided in two forms: vanilla JavaScript and jQuery-based code. This is more than a design choice: I'd like to stress the fact that with today's browsers even plain JavaScript can be an excellent choice, especially when we combine JavaScript with CSS transitions and animations. jQuery is also an excellent choice when we want to get things done without worrying too much about browser incompatibilities or desire a leaner API than that of native DOM. The JavaScript code provided below is merely demonstrative: it serves only for the sake of the examples.

In that vein, I'd like to say a word on the plain JavaScript examples: in these examples I use objects with a very simple initialization method, namely the init() method (a famous yet overused name in many environments). This method takes care of invoking the required code when you instantiate the object with the new operator. If you're not familiar with the prototype property used here, don't worry: this thread on Stack Overflow will get you started.. Why objects and not functions? It would take another article to answer this question, but for the included examples the best answer is simply keeping the code more organized and reusable.

Summary

The HTML structure

It's important to keep in mind that our HTML markup must make sense without CSS and JavaScript enabled. In other words people should still be able to access our content even when they use a browser that doesn't support CSS and JavaScript (such as Lynx) or when CSS and JavaScript are disabled.

To accomplish this task, we need to know which components will be part of our structure. Typically, these components are:

  1. An outermost container element.
  2. An optional innermost wrapper element.
  3. Several elements that represent slides.
  4. An optional wrapper for pagination links.
  5. Two optional buttons for the "Previous" and "Next" actions.

The 2, 4 and 5 components are optional for the following reasons:

  • Slides can be wrapped only by a single element. This happens often when the presentation effect hinges on a fade in/fade out transition.
  • Pagination links and buttons can be omitted when the slideshow is automatic, meaning that the main animation take place without any user-interaction.

Here is an example of a possible HTML structure.


<div class="slider" id="main-slider"><!-- outermost container element -->
	<div class="slider-wrapper"><!-- innermost wrapper element -->
		<div class="slide">...</div><!-- slides -->
		<div class="slide">...</div>
		<div class="slide">...</div>
	</div>
	<div class="slider-nav"><!-- "Previous" and "Next" actions -->
		<button type="button" class="slider-previous">Previous</button>
		<button type="button" class="slider-next">Next</button>
	</div>
</div>

It's always a good thing to use classes on slideshow's elements because there might be several slideshows on the same page. To uniquely identify a slideshow you can set an ID on the outermost container element.

We use two button elements in the above markup because in this case using HTML links would not be appropriate: links would not point to existing locations so the best thing to do is using two scriptable elements (read You can't create a button by Nicholas Zakas for more details about this design choice).

If your slides contain only images, you can slightly modify our initial structure:


<div class="slider" id="main-slider"><!-- outermost container element -->
	<div class="slider-wrapper"><!-- innermost wrapper element -->
		<img src="image1.jpg" alt="First" class="slide" /><!-- slides -->
		<img src="image2.jpg" alt="Second" class="slide" />
		<img src="image3.jpg" alt="Third" class="slide" />
	</div>
	<div class="slider-nav"><!-- "Previous" and "Next" actions -->
		<button type="button" class="slider-previous">Previous</button>
		<button type="button" class="slider-next">Next</button>
	</div>
</div>

Don't forget to always add a meaningful value to the alt attribute of each image. This simple practice will allow screen readers to read aloud your descriptions instead of simply getting no content at all or even worse, reading the URL of the image.

To use pagination links, you can change the initial markup as follows:


<div class="slider" id="main-slider"><!-- outermost container element -->
	<div class="slider-wrapper"><!-- innermost wrapper element -->
		<div class="slide" id="slide-1">...</div><!-- slides -->
		<div class="slide" id="slide-2">...</div>
		<div class="slide" id="slide-3">...</div>
	</div>
	<div class="slider-nav"><!-- pagination links -->
		<a href="#slide-1">1</a>
		<a href="#slide-2">2</a>
		<a href="#slide-3">3</a>
	</div>
</div>

Now each pagination link points to a different slide thanks to the anchor set on its href attribute. This is intentional: we want each link to work without JavaScript.

There are also slideshows that combine both pagination links and controls:


<div class="slider" id="main-slider"><!-- outermost container element -->
	<div class="slider-wrapper"><!-- innermost wrapper element -->
	    <!-- slides -->
		<div class="slide" id="slide-1" data-image="image1.jpg"></div>
		<div class="slide" id="slide-2" data-image="image2.jpg"></div>
		<div class="slide" id="slide-3" data-image="image3.jpg"></div>
	</div>
	<!-- "Previous" and "Next" actions -->
	<div class="slider-nav">
		<button type="button" class="slider-previous">Previous</button>
		<button type="button" class="slider-next">Next</button>
	</div>
	<!-- pagination -->
	<div class="slider-pagination">
		<a href="#slide-1">1</a>
		<a href="#slide-2">2</a>
		<a href="#slide-3">3</a>
	</div>
</div>	

Note how each pagination link has an actual anchor that points to a specific slide. This structure is merely indicative: to increase accessibility you can place the pagination block just before the innermost wrapper element so that it works just as a navigation system. Then you can position the pagination structure with CSS.

Note also the use of data attributes: some slideshows can insert images as background images so these attributes will be used by JavaScript as placeholders for associating a background image to each slide.

Semantic tip: using lists

If we consider slides as a series of elements, then using lists can be a more semantic choice. In this case our initial structure will be changed as follows:


<ul class="slider-wrapper"><!-- innermost wrapper element -->
		<li class="slide" id="slide-1">...</li><!-- slides -->
		<li class="slide" id="slide-2">...</li>
		<li class="slide" id="slide-3">...</li>
</ul>

If your slides come in a well-defined order (e.g. such as a presentation), then you can also use ordered lists (<ol>).

The CSS code

Let's start with the following HTML structure:


<div class="slider" id="main-slider"><!-- outermost container element -->
	<div class="slider-wrapper"><!-- innermost wrapper element -->
		<img src="image1.jpg" alt="First" class="slide" /><!-- slides -->
		<img src="image2.jpg" alt="Second" class="slide" />
		<img src="image3.jpg" alt="Third" class="slide" />
	</div>
	<div class="slider-nav"><!-- "Previous" and "Next" actions -->
		<button type="button" class="slider-previous">Previous</button>
		<button type="button" class="slider-next">Next</button>
	</div>
</div>

Our slideshow will slide from right to left. This means that the outermost element will have a stated dimension while the innermost wrapper element will be wider in order to contain all the slides.

Only the first slide will be initially visible. We can do this thanks to the CSS overflow property:


.slider {
	width: 1024px;
	overflow: hidden;
}

.slider-wrapper {
	width: 9999px;
	height: 683px;
	position: relative;
	transition: left 500ms linear;
}

The innermost wrapper element has the following important styles:

  • A larger width to contain all the slides.
  • A stated height to correctly set a maximum height for the slides and to apply clearance to the floated slides.
  • A relative positioning that will create the sliding movement from right to left.
  • A CSS transition on the left property that will allow a fluid sliding movement. For the sake of the example, we did not include all the vendor-specific prefixes. Note that you can also use CSS transformations to do this (with a translation).

As said earlier, all the slides are floated in order to allow them to be aligned on the same line. Further, they're also relatively positioned because we want to get their left offset with JavaScript. This offset will be used to make the innermost wrapper element slide and reveal the current image.


.slide {
	float: left;
	position: relative;
	width: 1024px;
	height: 683px;
}

Declaring a larger width doesn't imply that we have to necessarily keep that width: JavaScript can be used to set the required width by multiplying the width of a single slide by the total number of slides. Though 9999 pixels may seem a reasonable length, we don't always know in advance how many slides there will be in our slideshow so we have to think dynamically.

In our markup the navigation system is made up of a "Previous" and "Next" buttons. When styling button elements we have to first reset their default styles and then apply our rules:


.slider-nav {
	height: 40px;
	width: 100%;
	margin-top: 1.5em;
}

.slider-nav button {
	border: none;
	display: block;
	width: 40px;
	height: 40px;
	cursor: pointer;
	text-indent: -9999em;
	background-color: transparent;
	background-repeat: no-repeat;
}

.slider-nav button.slider-previous {
	float: left;
	background-image: url(previous.png);
}

.slider-nav button.slider-next {
	float: right;
	background-image: url(next.png);
}

If you use pagination links instead of buttons, you can change the above styles as follows:


.slider-nav {
	text-align: center;
	margin-top: 1.5em;
}

.slider-nav a {
	display: inline-block;
	text-decoration: none;
	border: 1px solid #ddd;
	color: #444;
	width: 2em;
	height: 2em;
	line-height: 2;
	text-align: center;	
}
.slider-nav a.current {
	border-color: #000;
	color: #000;
	font-weight: bold;
}

The current class will be dynamically applied by JavaScript when a slide is selected.

This kind of setup is most common when we want to achieve the default sliding effect (a movement from right to left). If we want to create a fading effect, we have to change our initial rules because floating adds an horizontal offset between each slide.

In short, we don't need a single line of slides anymore. What we need now is a stack of slides. This can be achieved by using contextual positioning:


.slider {
	width: 1024px;
	margin: 2em auto;
	
}

.slider-wrapper {
	width: 100%;
	height: 683px;
	position: relative; /* Creates a context for absolute positioning */
}

.slide {
	position: absolute; /* All slides are absolutely positioned */
	width: 100%;
	height: 100%;
	opacity: 0; /* All slides are hidden */
	transition: opacity 500ms linear;
}

/* Only the 1st slide is initially visible */

.slider-wrapper > .slide:first-child {
	opacity: 1;
}

We use the opacity property to hide the slides because screen readers don't read aloud the content of elements with display: none (consult CSS in Action: Invisible Content Just for Screen Reader Users by WebAIM for more details about hiding techniques ).

What we did is simple: thanks to CSS contextual positioning we've created a stack of slides where the slide that comes last in source is displayed in front of the others. This is not what we want. To preserve the visual order of our slideshow we have to hide all the slides except the slide that comes first in source.

JavaScript will simply trigger the CSS transition by changing the value of the opacity property on the current slide while resetting to 0 the opacity of all other slides.

IE9 compatibility issues

IE9 doesn't support CSS transitions. Changing the value of a CSS property will simply make that property change without any intermediate step. If we want to keep the original effect even in IE9, we should consider using jQuery.

For an overview of the possible solutions, take a look at this thread on Stack Overflow.

The JavaScript code

Slideshows without pagination

Slideshows without a pagination system simply rely on two actions (or controls), namely the "Next" and "Previous" buttons. These buttons can be seen as increment and decrement operators, respectively.

There's always a pointer (or cursor) that will be incremented or decremented every time a user clicks on these buttons. The pointer is initially set to 0 and its purpose is to select the current slide exactly as we do with arrays.

So when we click for the first time on the "Next" button, the pointer will be incremented by 1 and we can get the second slide (remember that we're dealing with an array-like structure). Then we click on the "Previous" button and the pointer is decremented by 1 so we get the first slide. And so on.

We'll be using the jQuery's .eq() method together with our pointer to select the current slide. In plain JavaScript this will become:


function Slideshow( element ) {
	this.el = document.querySelector( element );
	this.init();	
}

Slideshow.prototype = {
	init: function() {
		this.slides = this.el.querySelectorAll( ".slide" );
		//...
	},
	_slideTo: function( pointer ) {
		var currentSlide = this.slides[pointer];
		//...
	}
};

Remember that a NodeList uses indexes just like an actual array. Another way to select the current slide in plain JavaScript is to make use of CSS3 selectors:


Slideshow.prototype = {
	init: function() {
		//...
	},
	_slideTo: function( pointer ) {
	
        var n = pointer + 1;	
		var currentSlide = this.el.querySelector( ".slide:nth-child(" + n + ")" );
		//...
	}
};

The CSS3 :nth-child() selector counts elements starting from 1, not from 0 so we need to increment by 1 our pointer before using it to select the current slide.

Once selected the current slide we need to make its parent container move from right to left. In jQuery we can use the .animate() method with a negative value for the left property:


(function( $ ) {
	$.fn.slideshow = function( options ) {
		options = $.extend({
			wrapper: ".slider-wrapper",
			slides: ".slide",
			//...
			speed: 500,
			easing: "linear"
		}, options);
		
		var slideTo = function( slide, element ) {
			var $currentSlide = $( options.slides, element ).eq( slide );
			
			$( options.wrapper, element ).
			animate({
				left: - $currentSlide.position().left
			}, options.speed, options.easing );	
			
		};

        //...
	};

})( jQuery );

We use the current left offset of the selected slide to create our sliding effect. In plain JavaScript there's no native .animate() method, so the best thing we can do is to take advantage of CSS transitions:


.slider-wrapper {
	position: relative; // Required
	transition: left 500ms linear;
}

Then we can simply change the left property dynamically using the style object:


function Slideshow( element ) {
	this.el = document.querySelector( element );
	this.init();	
}

Slideshow.prototype = {
	init: function() {
	    this.wrapper = this.el.querySelector( ".slider-wrapper" );
		this.slides = this.el.querySelectorAll( ".slide" );
		//...
	},
	_slideTo: function( pointer ) {
		var currentSlide = this.slides[pointer];
		this.wrapper.style.left = "-" + currentSlide.offsetLeft + "px";
	}
};

Now it's time to bind a click event to each control. In jQuery we can use the .on() method whereas in plain JavaScript we'll attach our events through the addEventListener() method.

We also need to check whether our pointer has reached a specific limit, namely 0 for the "Previous" button and the total number of slides for the "Next" button. In both cases the "Previous" or the "Next" button must be shown or hidden and the pointer must be reset to the appropriate value:


(function( $ ) {
	$.fn.slideshow = function( options ) {
		options = $.extend({
			wrapper: ".slider-wrapper",
			slides: ".slide",
			previous: ".slider-previous",
			next: ".slider-next",
			//...
			speed: 500,
			easing: "linear"
		}, options);
		
		var slideTo = function( slide, element ) {
			var $currentSlide = $( options.slides, element ).eq( slide );
			
			$( options.wrapper, element ).
			animate({
				left: - $currentSlide.position().left
			}, options.speed, options.easing );	
			
		};

        return this.each(function() {
			var $element = $( this ),
				$previous = $( options.previous, $element ),
				$next = $( options.next, $element ),
				index = 0,
				total = $( options.slides ).length;
				
			$next.on( "click", function() {
				index++;
				$previous.show();
				
				if( index == total - 1 ) {
					index = total - 1;
					$next.hide();	
				}
				
				slideTo( index, $element );	
				
			});
			
			$previous.on( "click", function() {
				index--;
				$next.show();
				
				if( index == 0 ) {
					index = 0;
					$previous.hide();	
				}
				
				slideTo( index, $element );	
				
			});

				
		});
	};

})( jQuery );

In plain JavaScript this becomes:


function Slideshow( element ) {
	this.el = document.querySelector( element );
	this.init();	
}

Slideshow.prototype = {
	init: function() {
	    this.wrapper = this.el.querySelector( ".slider-wrapper" );
		this.slides = this.el.querySelectorAll( ".slide" );
		this.previous = this.el.querySelector( ".slider-previous" );
		this.next = this.el.querySelector( ".slider-next" );
		this.index = 0;
		this.total = this.slides.length;
			
		this.actions();	
	},
	_slideTo: function( pointer ) {
		var currentSlide = this.slides[pointer];
		this.wrapper.style.left = "-" + currentSlide.offsetLeft + "px";
	},
	actions: function() {
		var self = this;
		self.next.addEventListener( "click", function() {
			self.index++;
			self.previous.style.display = "block";
				
			if( self.index == self.total - 1 ) {
				self.index = self.total - 1;
				self.next.style.display = "none";
			}
				
			self._slideTo( self.index );
				
		}, false);
			
		self.previous.addEventListener( "click", function() {
			self.index--;
			self.next.style.display = "block";
				
			if( self.index == 0 ) {
				self.index = 0;
				self.previous.style.display = "none";
			}
				
			self._slideTo( self.index );
				
		}, false);
	}

};

Examples

Slideshows with pagination

In slideshows with pagination each pagination link corresponds to a single slide, so there's no more need for a pointer. Each link may have an hash or an attribute's value that points to a specific slide.

Animations don't change. What is different here is the way by which a user navigates the slides. In jQuery we may have the following code:


(function( $ ) {
	$.fn.slideshow = function( options ) {
		options = $.extend({
			wrapper: ".slider-wrapper",
			slides: ".slide",
			nav: ".slider-nav",
			speed: 500,
			easing: "linear"
		}, options);
		
		var slideTo = function( slide, element ) {
			var $currentSlide = $( options.slides, element ).eq( slide );
			
			$( options.wrapper, element ).
			animate({
				left: - $currentSlide.position().left
			}, options.speed, options.easing );	
			
		};

        return this.each(function() {
			var $element = $( this ),
				$navigationLinks = $( "a", options.nav );
				
				$navigationLinks.on( "click", function( e ) {
					e.preventDefault();
					var $a = $( this ),
						$slide = $( $a.attr( "href" ) );
						
						slideTo( $slide, $element );
						$a.addClass( "current" ).siblings().
						removeClass( "current" );
					
				});
				
				
		});
	};

})( jQuery );

In this case each link's anchor corresponds to the ID of a specific slide. We can use this anchor also in plain JavaScript or a custom data attribute that stores the numeric index of each slide within the NodeList:


function Slider( element ) {
	this.el = document.querySelector( element );
	this.init();
}
Slider.prototype = {
	init: function() {
		this.links = this.el.querySelectorAll( "#slider-nav a" );
		this.wrapper = this.el.querySelector( "#slider-wrapper" );
		this.navigate();
	},
	navigate: function() {
		for ( var i = 0; i < this.links.length; ++i ) {
			var link = this.links[i];
			this.slide( link );
		}
	},
	slide: function( element ) {
		var self = this;
		element.addEventListener( "click", function( e ) {
			e.preventDefault();
			var a = this;
			self.setCurrentLink( a );
			var index = parseInt( a.getAttribute( "data-slide" ), 10 ) + 1;
			var currentSlide = self.el.querySelector( ".slide:nth-child(" + index + ")" );
			self.wrapper.style.left = "-" + currentSlide.offsetLeft + "px";
		},
		false);
	},
	setCurrentLink: function(link) {
		var parent = link.parentNode;
		var a = parent.querySelectorAll( "a" );
		link.className = "current"; 
		for ( var j = 0; j < a.length; ++j ) {
			var cur = a[j];
			if ( cur !== link ) {
				cur.className = "";
			}
		}
	}
};

From IE10 onward you can also manipulate CSS classes with classList:


link.classList.add( "current" );

From IE11 onward data attributes can also be retrieved by using the dataset property:


var index = parseInt( a.dataset.slide, 10 ) + 1;

Examples

Slideshows with pagination and controls

This kind of slideshows introduce a little challenge for our JavaScript code: in this case we have to combine the numerical cursor with the hash related to each pagination link. In other words, we should always select the current slide based both on the cursor's position and the slide selected by pagination links.

If we click on the third pagination link, the cursor's value should be set to 2 so that when we click on the "Previous" button we get back to the second slide (the value here for the cursor is 1 ). As you can see, it's just a matter of synchronization between our cursor and pagination links.

We can sync the cursor's value and pagination links by using the numerical index of each link in the DOM. Since every link corresponds to a single slide, their numerical index will be 0, 1, 2 and so on. So when we click on the third pagination link, whose index is 2, we set the cursor's value to 2 and if we click then on the "Previous" button, the cursor will be decremented by 1 and we can get the second slide.

But there's more: when we click on pagination links we should also perform the same routines on the cursor's value to check whether it has reached its limit and act accordingly.

In jQuery the code is as follows:


(function( $ ) {
	$.fn.slideshow = function( options ) {
		options = $.extend({
			//...
			pagination: ".slider-pagination",
			//...
			
		}, options);
		
		$.fn.slideshow.index = 0; 
		
		return this.each(function() {
			var $element = $( this ),
			    //...
			    $pagination = $( options.pagination, $element ),
			    $paginationLinks = $( "a", $pagination ),
			    //...
			    
		    $paginationLinks.on( "click", function( e ) {
				e.preventDefault();
				var $a = $( this ),
					elemIndex = $a.index(); // DOM numerical index
					$.fn.slideshow.index = elemIndex;
					
					if( $.fn.slideshow.index > 0 ) {
						$previous.show();
						
					} else {
						$previous.hide();
					}
					
					if( $.fn.slideshow.index == total - 1 ) {
						$.fn.slideshow.index = total - 1;
						$next.hide();	
					} else {
						$next.show();
					}
					
					
					
					slideTo( $.fn.slideshow.index, $element );
					$a.addClass( "current" ).
					siblings().removeClass( "current" );
				
			});

	    });
		
	};
	//...
})( jQuery );

The first noticeable change in our code is the visibility of our cursor: now index has been declared as a property of the slideshow object. By doing this we avoid problems with the scope created by jQuery's callbacks used with the event methods. The cursor is now available globally within the namespace created by our plugin. Further, it is also available outside the plugin's namespace because it has been declared as a public property of the slideshow object.

To get the numerical index of each pagination link within the DOM we use the jQuery's .index() method. Then we use this value to update our cursor's value.

In JavaScript there's no .index() method, so the quickest solution is to use data attributes set with the cursor used in a for loop:


(function() {
	
	function Slideshow( element ) {
		this.el = document.querySelector( element );
		this.init();
	}
	
	Slideshow.prototype = {
		init: function() {
			this.wrapper = this.el.querySelector( ".slider-wrapper" );
			this.slides = this.el.querySelectorAll( ".slide" );
			this.previous = this.el.querySelector( ".slider-previous" );
			this.next = this.el.querySelector( ".slider-next" );
			this.navigationLinks = this.el.querySelectorAll( ".slider-pagination a" );
			this.index = 0;
			this.total = this.slides.length;
			
			this.setup();
			this.actions();	
		},
	//...
	    setup: function() {
		    var self = this;
		    //...
		    for( var k = 0; k < self.navigationLinks.length; ++k ) {
				var pagLink = self.navigationLinks[k];
				pagLink.setAttribute( "data-index", k );
				// Or: pagLink.dataset.index = k;
			}	
	    },
	    //...
    };
})();

Then we can attach our routines to pagination links and use the newly created data attributes:


actions: function() {
			
	var self = this;
	
	//...
	
	for( var i = 0; i < self.navigationLinks.length; ++i ) {
		var a = self.navigationLinks[i];
				
		a.addEventListener( "click", function( e ) {
			e.preventDefault();
			var n = parseInt( this.getAttribute( "data-index" ), 10 );
			// Or: var n = parseInt( this.dataset.index, 10 );
					
			self.index = n;	
					
			if( self.index == 0 ) {
				self.index = 0;
				self.previous.style.display = "none";
			}
					
			if( self.index > 0 ) {
				self.previous.style.display = "block";	
			}
					
			if( self.index == self.total - 1 ) {
				self.index = self.total - 1;
				self.next.style.display = "none";
			} else {
				self.next.style.display = "block";	
			}
									
			self._slideTo( self.index );
					
			self._highlightCurrentLink( this );
					
					
					
			}, false);
		}
}

Examples

Dealing with dimensions

Let's get back for a moment to the following CSS rule:


.slider-wrapper {
	width: 9999px;
	height: 683px;
	position: relative;
	transition: left 500ms linear;
}

What if there are many slides? Simply put, 9999 pixels won't be probably enough. For that reason we ha to dynamically adjust slide dimensions based on the width of each slide and the total number of slides contained within our slideshow.

With jQuery is pretty straightforward:


// Full width slideshow

return this.each(function() {
	var $element = $( this ),
		total = $( options.slides ).length;
		//...
		$( options.slides, $element ).width( $( window ).width() );
		$( options.wrapper, $element ).width( $( window ).width() * total );
		//...
});

We've simply got the global width of the browser's viewport and used this value to set the width of each slide. To set the total width of the inner wrapper we've multiplied the viewport's width by the total number of slides.


// Fixed width slideshow

return this.each(function() {
	var $element = $( this ),
		total = $( options.slides ).length;
		//...
	
		$( options.wrapper, $element ).width( $( options.slides ).eq( 0 ).width() * total );
		//...
});

Here the start width is given by the width of each slide. We need only to set the total width of the inner wrapper.

Now the innermost container is wide enough to host all of its child slides. This can be done also in plain JavaScript with minor changes:


// Full width slideshow

Slideshow.prototype = {
	init: function() {
	    this.wrapper = this.el.querySelector( ".slider-wrapper" );
		this.slides = this.el.querySelectorAll( ".slide" );
		//...
		this.total = this.slides.length;
		
		this.setDimensions();	
		this.actions();	
	},
	setDimensions: function() {
		var self = this;
		// Viewport's width
		var winWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
		var wrapperWidth = winWidth * self.total;
		for( var i = 0; i < self.total; ++i ) {
			var slide = self.slides[i];
			slide.style.width = winWidth + "px";
		}
		self.wrapper.style.width = wrapperWidth + "px";
	},
	//...
	
	
};


// Fixed width slideshow

Slideshow.prototype = {
	init: function() {
	    this.wrapper = this.el.querySelector( ".slider-wrapper" );
		this.slides = this.el.querySelectorAll( ".slide" );
		//...
		this.total = this.slides.length;
		
		this.setDimensions();	
		this.actions();	
	},
	setDimensions: function() {
		var self = this;
		var slideWidth = self.slides[0].offsetWidth; // Single slide's width
		var wrapperWidth = slideWidth * self.total;
		self.wrapper.style.width = wrapperWidth + "px";
	},
	//...
	
	
};

Fading effects

A fading effect is a common effect in JavaScript slideshows. When the current slide fades out, the next slide fades in and so on. jQuery provides the fadeIn() and fadeOut() methods. These methods not only work on the CSS opacity property but also adjust the display property so that an element will be completely removed from the layout once the animation is complete (display:none).

With plain JavaScript the quickest solution is to rely on the opacity property and use the CSS positioning stack (stacking one slide in top of another). In this case the first slide will be initially visible (opacity: 1) while the other slides will be hidden (opacity:0).

The following CSS code shows a common setup:


.slider {
	width: 100%;
	overflow: hidden;
	position: relative;
	height: 400px;
}

.slider-wrapper {
	width: 100%;
	height: 100%;
	position: relative;
}

.slide {
	position: absolute;
	width: 100%;
	height: 100%;
	opacity: 0;
}

.slider-wrapper > .slide:first-child {
	opacity: 1;
}

In plain JavaScript we need to register a CSS transition on each slide:


.slide {
	float: left;
	position: absolute;
	width: 100%;
	height: 100%;
	opacity: 0;
	transition: opacity 500ms linear;
}

In jQuery if you want to use the fadeIn() and fadeOut() methods, you need to change opacity with display:


.slide {
	float: left;
	position: absolute;
	width: 100%;
	height: 100%;
	display: none;
}

.slider-wrapper > .slide:first-child {
	display: block;
}

In jQuery the code is as follows:


(function( $ ) {
	$.fn.slideshow = function( options ) {
	
	    options = $.extend({
			wrapper: ".slider-wrapper",
			previous: ".slider-previous",
			next: ".slider-next",
			slides: ".slide",
			nav: ".slider-nav",
			speed: 500,
			easing: "linear"
			
		}, options);
	
		var slideTo = function( slide, element ) {
			var $currentSlide = $( options.slides, element ).eq( slide );
			
			$currentSlide.
			animate({
				opacity: 1
			}, options.speed, options.easing ).
			siblings( options.slides ).
			css( "opacity", 0 );	
			
		};
		
		//...
	};

})( jQuery );


When we animate the opacity property of the current slide we also need to reset the same property on all the other slides to prevent overlapping.

In plain JavaScript this becomes:


Slideshow.prototype = {
	//...
	_slideTo: function( slide ) {
		var currentSlide = this.slides[slide];
		currentSlide.style.opacity = 1;
			
		for( var i = 0; i < this.slides.length; i++ ) {
			var slide = this.slides[i];
			if( slide !== currentSlide ) {
				slide.style.opacity = 0;
			}
		}
	},
	//...
};

Examples

Media elements: videos

We can include also videos in our slideshows. Here's how our markup will be defined if we decide to use Vimeo's videos:


<div class="slider-wrapper"><!-- innermost wrapper element -->
		<div class="slide">
			<iframe src="https://player.vimeo.com/video/109608341?title=0&amp;byline=0&amp;portrait=0" width="1024" height="626" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
		</div><!-- slides -->
		<div class="slide">
			<iframe src="https://player.vimeo.com/video/102570343?title=0&amp;byline=0&amp;portrait=0" width="1024" height="576" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
		</div>
		<div class="slide">
			<iframe src="https://player.vimeo.com/video/97620325?title=0&amp;byline=0&amp;portrait=0" width="1024" height="576" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
		</div>
</div>

In this case our videos are embedded via the iframe element. This element is an inline-block, replaced element just like an image. Replaced means that its content is taken from an external source.

If we want to create a full page video slideshow, we have to change our CSS styles as follows:


html, body {
	margin: 0;
	padding: 0;
	height: 100%;
	min-height: 100%; /* Make sure the page's height is at its fullest */
}
.slider {
	width: 100%;
	overflow: hidden;
	height: 100%;
	min-height: 100%; /* Full width and height */
	position: absolute; /* Absolutely positioned */
}

.slider-wrapper {
	width: 100%;
	height: 100%; /* Full width and height */
	position: relative;
}

.slide {
	float: left;
	position: absolute;
	width: 100%;
	height: 100%;
}

.slide iframe {
	display: block; /* Must be a block-level element */
	position: absolute; /* Absolutely positioned */
	width: 100%;
	height: 100%; /* Full width and height */
}

Examples

Automatic slideshows

Automatic slideshows make use of JavaScript timers to work. Every time the callback function created by the setInterval() function runs, our cursor will be incremented by 1, thus selecting the next slide.

Problem: what happens when the numeric value of our cursor equals the total number of slides? It must be reset to 0 in order to make the sequence start again.

Another aspect is the duration of each timer interval: the amount of time should be always equal to the total duration of the animation set on each slide. So if we have an animation that takes 500 milliseconds to complete, the value of the second parameter of the setInterval() function should be set to 500. If both time intervals don't match, your slideshow will start behaving randomly.

Another problem with automatic slideshows is that they tend to be annoying on the long run for users because they simply can't stop them. A good solution to this problem is to stop the sliding sequence when the user's mouse is over the slideshow and resume that sequence when the mouse cursor leaves the slideshow's block.

In jQuery a basic solution is as follows:


(function( $ ) {
	$.fn.slideshow = function( options ) {
		options = $.extend({
			slides: ".slide",
			speed: 3000,
			easing: "linear"
			
		}, options);
		
		var timer = null; // Our timer
		var index = 0;    // Our cursor
		
		var slideTo = function( slide, element ) {
			var $currentSlide = $( options.slides, element ).eq( slide );
			
			$currentSlide.stop( true, true ).
			animate({
				opacity: 1
			}, options.speed, options.easing ).
			siblings( options.slides ).
			css( "opacity", 0 );	
			
		};
		
		var autoSlide = function( element ) {
			// Initializes the sequence
			timer = setInterval(function() {
				index++; // Increments the cursor
				if( index == $( options.slides, element ).length ) {
					index = 0; // Resets the cursor
				}
				slideTo( index, element );
			}, options.speed); // Same interval of the .animate() method	
		};
		
		var startStop = function( element ) {
			element.hover(function() { // Stops the sequence on hover
				clearInterval( timer );
				timer = null;	
			}, function() {
				autoSlide( element ); // Restarts the sequence	
			});
		};
		
		return this.each(function() {
			var $element = $( this );
				
				autoSlide( $element );
				startStop( $element );
			
		});
	};
	
})( jQuery );

We've used the .stop() method with both parameters set to true because we don't want to create an animation queue within our sequence.

In plain JavaScript our code becomes a little bit simpler. First, we register a CSS transition on each slide with the appropriate duration:


.slide {
	transition: opacity 3s linear; /* 3 seconds = 3000 milliseconds */
}

Then we can write the following code:


(function() {
	
	function Slideshow( element ) {
		this.el = document.querySelector( element );
		this.init();
	}
	
	Slideshow.prototype = {
		init: function() {
			this.slides = this.el.querySelectorAll( ".slide" );
			this.index = 0; // Our cursor
			this.timer = null;  // Our timer
			
			this.action();
			this.stopStart();	
		},
		_slideTo: function( slide ) {
			var currentSlide = this.slides[slide];
			currentSlide.style.opacity = 1;
			
			for( var i = 0; i < this.slides.length; i++ ) {
				var slide = this.slides[i];
				if( slide !== currentSlide ) {
					slide.style.opacity = 0;
				}
			}
		},
		action: function() {
			var self = this;
			// Initializes the sequence
			self.timer = setInterval(function() {
				self.index++; // Increments the cursor
				if( self.index == self.slides.length ) {
					self.index = 0; // Resets the cursor
				}
				self._slideTo( self.index );
				
			}, 3000); // Same interval of the CSS transition
		},
		stopStart: function() {
			var self = this;
			// Stops the sequence on hover
			self.el.addEventListener( "mouseover", function() {
				clearInterval( self.timer );
				self.timer = null;
				
			}, false);
			// Restarts the sequence
			self.el.addEventListener( "mouseout", function() {
				self.action();
				
			}, false);
		}
		
		
	};
})();

Examples

Keyboard navigation

Some advanced slideshows also provide keyboard navigation, namely the ability to navigate slides by pressing one or two keys. From a pure coding perspective, implementing keyboard navigation simply implies that we have to bind an action to the keydown event.

Keyboards events come with the keyCode property for the event object. This property returns a numeric code which tells us which key was pressed (consult this page for a complete list of key codes).

In our previous examples we've already attached an action to the "Previous" and "Next" buttons. Now we want to attach the same action to the left and right arrow keys. In jQuery the solution is simple:


$( "body" ).on( "keydown", function( e ) {
	var code = e.keyCode;
	if( code == 39 ) { // Left arrow
		$next.trigger( "click" );
	}
	if( code == 37 ) { // Right arrow
		$previous.trigger( "click" );
	}
				
});

We simply trigger the click event on the corresponding button. In plain JavaScript the procedure is the same but we don't have the easy jQuery's .trigger() method: we have to use the dispatchEvent() method in the context of mouse events:


document.body.addEventListener( "keydown", function( e ) {
	var code = e.keyCode;
	var evt = new MouseEvent( "click" );  // click event
				
	if( code == 39 ) { // Left arrow
		self.next.dispatchEvent( evt );
	}
	if( code == 37 ) { // Right arrow
		self.previous.dispatchEvent( evt );
	}
				
}, false);

Note that in a proper application architecture this is considered bad practice. We should expose the actual functionality run by clicking on a button in a public callable method and simply have the click handler of the button invoke that function. This way any other part of the application wanting to use the feature, does not have to fake DOM events but can call the method directly.

Examples

Callbacks

Clicking on a button, selecting a slide, creating an animation or a transition, almost every action that takes place on a slideshow is useful. But it would be more useful if we could attach some custom code when such actions take place.

This is the purpose of callbacks: functions that are executed only on certain actions. Suppose that we have a slideshow with captions. Such captions are initially hidden. When the sliding animation takes place, we want to show the current slide's caption or even doing something with each slide.

In jQuery we can create a callback as follows:


(function( $ ) {
	$.fn.slideshow = function( options ) {
		options = $.extend({
			//...
			callback: function() {}
			
		}, options);
		
		var slideTo = function( slide, element ) {
			var $currentSlide = $( options.slides, element ).eq( slide );
			
			$currentSlide.
			animate({
				opacity: 1
			}, options.speed, 
			   options.easing,
			   // Callback on the current slide 
			   options.callback( $currentSlide ) ).
			   siblings( options.slides ).
			   css( "opacity", 0 );	
			
		};

        //...
    };
})( jQuery );

In this case our callback is executed as the callback function of the .animate() method and takes the current slide as its argument. Here's how it can be used:


$(function() {
	$( "#main-slider" ).slideshow({
		callback: function( slide ) {
			var $wrapper = slide.parent();
			// Shows the current caption and hides the other
			$wrapper.find( ".slide-caption" ).hide();
			slide.find( ".slide-caption" ).show( "slow" );
		}
			
	});
});

In plain JavaScript this becomes:


(function() {
	
	function Slideshow( element, callback ) {
		this.callback = callback || function() {}; // Our callback
		this.el = document.querySelector( element );
		this.init();
	}
	
	Slideshow.prototype = {
		init: function() {
			//...
			this.slides = this.el.querySelectorAll( ".slide" );
			//...
			
			//...
		},
		_slideTo: function( slide ) {
			var self = this;
			var currentSlide = self.slides[slide];
			currentSlide.style.opacity = 1;
			
			for( var i = 0; i < self.slides.length; i++ ) {
				var slide = self.slides[i];
				if( slide !== currentSlide ) {
					slide.style.opacity = 0;
				}
			}
			setTimeout( self.callback( currentSlide ), 500 );
			// Invokes our callback once the transition is complete
		}
		
	};
    //
})();

Our callback has been declared as the second parameter of our constructor function. It can be used as follows:


document.addEventListener( "DOMContentLoaded", function() {
		
		var slider = new Slideshow( "#main-slider", function( slide ) {
			var wrapper = slide.parentNode;
			
			// Shows the current caption and hides the other
			
			var allSlides = wrapper.querySelectorAll( ".slide" );
			var caption = slide.querySelector( ".slide-caption" );
			caption.classList.add( "visible" );
			
		   for( var i = 0; i < allSlides.length; ++i ) {
			  var sld = allSlides[i];
			  var cpt = sld.querySelector( ".slide-caption" );
		   if( sld !== slide ) {
			cpt.classList.remove( "visible" );
		   }
		}	
			
			
	});
		
});

Examples

Using external APIs

So far we've discussed a very simple scenario: all the slideshow's contents are already inserted in our document. If we want to use external APIs (YouTube, Vimeo, Flickr etc.), we have to bear in mind that our slideshow must be dynamically populated with slides once we've retrieved the external contents (usually delivered in the form of a JSON or JSONP feed).

Since the response from the remote server may not be immediate, we have to insert an animated loader to show that an activity is in progress in order to address user's needs:


<div class="slider" id="main-slider"><!-- outermost container element -->
	<div class="slider-wrapper"><!-- innermost wrapper element -->
			
	</div>
	<div class="slider-nav"><!-- "Previous" and "Next" actions -->
		<button class="slider-previous">Previous</button>
		<button class="slider-next">Next</button>
	</div>
	<div id="spinner"></div><!-- Animated loader -->
</div>	

The animated loader can either be a GIF image or a pure CSS loader:


#spinner {
	border-radius: 50%;
	border: 2px solid #000;
	height: 80px;
	width: 80px;
	position: absolute;
	top: 50%;
	left: 50%;
	margin: -40px 0 0 -40px;
}
#spinner:after {
	content: '';
	position: absolute;
	background-color: #000;
	top:2px;
	left: 48%;
	height: 38px;
	width: 2px;
	border-radius: 5px;
	-webkit-transform-origin: 50% 97%;
			transform-origin: 50% 97%;
	-webkit-animation: angular 1s linear infinite;
			animation: angular 1s linear infinite;
}

@-webkit-keyframes angular {
    0%{-webkit-transform:rotate(0deg);}
    100%{-webkit-transform:rotate(360deg);}
}

@keyframes angular {
    0%{transform:rotate(0deg);}
    100%{transform:rotate(360deg);}
}

#spinner:before {
	content: '';
	position: absolute;
	background-color: #000;
	top:6px;
	left: 48%;
	height: 35px;
	width: 2px;
	border-radius: 5px;
	-webkit-transform-origin: 50% 94%;
			transform-origin: 50% 94%;
	-webkit-animation: ptangular 6s linear infinite;
			animation: ptangular 6s linear infinite;
}

@-webkit-keyframes ptangular {
    0%{-webkit-transform:rotate(0deg);}
    100%{-webkit-transform:rotate(360deg);}
}

@keyframes ptangular {
    0%{transform:rotate(0deg);}
    100%{transform:rotate(360deg);}
}

Our steps will be as follows:

  1. Fetch remote data
  2. Hide the loader
  3. Parse data
  4. Build the HTML content
  5. Populate the slideshow
  6. Handle slideshow

We want to retrieve the latest videos of a YouTube's user. In jQuery the code is as follows:


(function( $ ) {
	$.fn.slideshow = function( options ) {
		options = $.extend({
			wrapper: ".slider-wrapper",
		    //...
			loader: "#spinner",
			//...
			limit: 5,
			username: "learncodeacademy"
			
		}, options);
		
		//...
		
		var getVideos = function() {
			// Get YouTube videos
			var ytURL = "https://gdata.youtube.com/feeds/api/videos?alt=json&author=" + options.username + "&max-results=" + options.limit;
			$.getJSON( ytURL, function( videos ) { // Get videos as a JSON object
				$( options.loader ).hide(); // Hide the loader
				var entries = videos.feed.entry;
				var html = "";
				for( var i = 0; i < entries.length; ++i ) { // Parse data and build an HTML string
					var entry = entries[i];
					var idURL = entry.id.$t;
					var idVideo = idURL.replace( "http://gdata.youtube.com/feeds/api/videos/", "" );
					var ytEmbedURL = "https://www.youtube.com/embed/" + idVideo + "?rel=0&showinfo=0&controls=0";
					
					html += "<div class='slide'>";
					html += "<iframe src='" + ytEmbedURL + "' frameborder='0' allowfullscreen></iframe>";
					html += "</div>";
				}
				
				$( options.wrapper ).html( html ); // Populate the slideshow
				
			});
	
			
		};
		
		return this.each(function() {
			//...
			getVideos();
			
			// Handle slideshow	
			
		});
    };
})( jQuery );

In JavaScript there's an extra step: create a method to fetch the remote JSON feed:


(function() {
	
	function Slideshow( element ) {
		this.el = document.querySelector( element );
		this.init();
	}
	
	Slideshow.prototype = {
		init: function() {
			this.wrapper = this.el.querySelector( ".slider-wrapper" );
			this.loader = this.el.querySelector( "#spinner" );
			//...
			this.limit = 5;
			this.username = "learncodeacademy";
			
			
		},
		_getJSON: function( url, callback ) {
			callback = callback || function() {};
		
			var request = new XMLHttpRequest();
			
			request.open( "GET", url, true );
			request.send( null );
			
			request.onreadystatechange = function() {
				if ( request.status == 200 && request.readyState == 4 ) {
					
					var data = JSON.parse( request.responseText ); // JSON object
					
					
					
					callback( data );
					
				} else {
					console.log( request.status );

				}
			};	
	
		},
		//...
		
	};
})();

Then the routines are similar:


(function() {
	
	function Slideshow( element ) {
		this.el = document.querySelector( element );
		this.init();
	}
	
	Slideshow.prototype = {
		init: function() {
			this.wrapper = this.el.querySelector( ".slider-wrapper" );
			this.loader = this.el.querySelector( "#spinner" );
			//...
			this.limit = 5;
			this.username = "learncodeacademy";
			
			this.actions();
			
		},
		_getJSON: function( url, callback ) {
			callback = callback || function() {};
		
			var request = new XMLHttpRequest();
			
			request.open( "GET", url, true );
			request.send( null );
			
			request.onreadystatechange = function() {
				if ( request.status == 200 && request.readyState == 4 ) {
					
					var data = JSON.parse( request.responseText ); // JSON object
					
					
					
					callback( data );
					
				} else {
					console.log( request.status );

				}
			};	
	
		},
		//...
		getVideos: function() {
			var self = this;
			// Get YouTube videos
			var ytURL = "https://gdata.youtube.com/feeds/api/videos?alt=json&author=" + self.username + "&max-results=" + self.limit;
			
			self._getJSON( ytURL, function( videos ) { // Get videos as a JSON object
				var entries = videos.feed.entry;
				var html = "";
				self.loader.style.display = "none";  // Hide the loader
				
				for( var i = 0; i < entries.length; ++i ) { // Parse data and build an HTML string
					var entry = entries[i];
					var idURL = entry.id.$t;
					var idVideo = idURL.replace( "http://gdata.youtube.com/feeds/api/videos/", "" );
					var ytEmbedURL = "https://www.youtube.com/embed/" + idVideo + "?rel=0&amp;showinfo=0&controls=0";
					
					html += "<div class='slide'>";
					html += "<iframe src='" + ytEmbedURL + "' frameborder='0' allowfullscreen></iframe>";
					html += "</div>";
				}
				
				self.wrapper.innerHTML = html; // Populate the slideshow
				
			});
				
		},
		actions: function() {
			var self = this;
			
			self.getVideos();
			
			// Handle slideshow	
		}
			

		
	};
})();

Examples

Conclusion

Slideshows are an interesting opportunity to enhance user experience. If not abused, they can allow users to quickly find the contents of our websites just with a few clicks. Also, slideshows such as Revolution Slider or Nivo Slider clearly show how to push slideshows effects to the limits, thus providing an excellent visual experience. But in order to build stunning slideshows we need to know the basics. Without this knowledge, no development is possible.

Complete examples

GitHub