Creare slideshow JavaScript: tutorial completo

Short link

In questo articolo tratteremo le basi degli slideshow JavaScript. Descriveremo i blocchi fondamentali che costituiscono uno slideshow JavaScript (HTML, CSS, JavaScript) e le tecniche usate per gestire gli slideshow.

Il codice JavaScript verrà sviluppato sia in semplice JavaScript che con jQuery. Il motivo è semplice: oggi possiamo combinare JavaScript con le animazioni e transizioni CSS ma allo stesso tempo abbiamo bisogno di implementare anche soluzioni che funzionino in tutti i browser.

Verrà usato un approccio OOP negli esempi. Se non avete familiarità con questo modo di scrivere il codice JavaScript potete iniziare da questo thread su Stack Overflow.

La struttura HTML

Dobbiamo tenere presente che la marcatura deve mantenere una struttura semantica anche in assenza di CSS e JavaScript. Per far questo dobbiamo conoscere quali componenti faranno parte della nostra struttura. Di solito questi componenti sono:

  1. Un contenitore esterno.
  2. Un contenitore interno (facoltativo).
  3. Le slide.
  4. I link della paginazione (facoltativi).
  5. Due bottoni per andare avanti ed indietro nella sequenza delle slide (facoltativi).

I componenti 2, 4 e 5 sono facoltativi per i seguenti motivi:

  • Le slide possono essere racchiuse da un singolo elemento, di solito quando l'effetto usato è di assolvenza/dissolvenza.
  • I link della paginazione e i bottoni possono essere omessi quando gli slideshow sono automatici.

Ecco una possibile struttura HTML:


<div class="slider" id="main-slider"><!-- contenitore esterno -->
	<div class="slider-wrapper"><!-- contenitore interno -->
		<div class="slide">...</div><!-- slide -->
		<div class="slide">...</div>
		<div class="slide">...</div>
	</div>
	<div class="slider-nav"><!-- "Precedente" e "Successivo" -->
		<button type="button" class="slider-previous">Precedente</button>
		<button type="button" class="slider-next">Successivo</button>
	</div>
</div>

Si raccomanda di usare le classi sugli elementi degli slideshow perché potrebbero esserci più slideshow nella stessa pagina. Per identificarli univocamente possiamo usare un ID sul contenitore esterno.

Usiamo elementi button perché i link in questo caso non punterebbero a nessuna risorsa effettiva: potete consultare You can't create a button di Nicholas Zakas per approfondire i motivi di questa scelta.

Se le slide sono immagini, la struttura può essere modificata come segue:


<div class="slider" id="main-slider"><!-- contenitore esterno -->
	<div class="slider-wrapper"><!-- contenitore interno -->
		<img src="image1.jpg" alt="Descrizione" class="slide" /><!-- slide -->
		<img src="image2.jpg" alt="Descrizione" class="slide" />
		<img src="image3.jpg" alt="Descrizione" class="slide" />
	</div>
	<div class="slider-nav"><!-- "Precedente" e "Successivo" -->
		<button type="button" class="slider-previous">Precedente</button>
		<button type="button" class="slider-next">Successivo</button>
	</div>
</div>

Bisogna sempre usare attributi alt sulle immagini: non dimentichiamo mai l'accessibilità della nostra implementazione. Per usare i link della paginazione possiamo modificare il codice come segue:


<div class="slider" id="main-slider"><!-- contenitore esterno -->
	<div class="slider-wrapper"><!-- contenitore interno -->
		<div class="slide" id="slide-1">...</div><!-- slide -->
		<div class="slide" id="slide-2">...</div>
		<div class="slide" id="slide-3">...</div>
	</div>
	<div class="slider-nav"><!-- link della paginazione -->
		<a href="#slide-1">1</a>
		<a href="#slide-2">2</a>
		<a href="#slide-3">3</a>
	</div>
</div>

Abbiamo fatto puntare ciascun link ad una slide grazie all'ancora impostata sull'attributo href. Ricordate che il nostro slideshow deve essere accessibile anche senza JavaScript.

Altri slideshow combinano i bottoni di controllo con i link della paginazione:


<div class="slider" id="main-slider"><!-- contenitore esterno -->
	<div class="slider-wrapper"><!-- contenitore interno -->
	    <!-- slide -->
		<div class="slide" id="slide-1"</div>
		<div class="slide" id="slide-2"</div>
		<div class="slide" id="slide-3"></div>
	</div>
	<!-- "Precedente" e "Successivo" -->
	<div class="slider-nav">
		<button type="button" class="slider-previous">Precedente</button>
		<button type="button" class="slider-next">Successivo</button>
	</div>
	<!-- link della paginazione -->
	<div class="slider-pagination">
		<a href="#slide-1">1</a>
		<a href="#slide-2">2</a>
		<a href="#slide-3">3</a>
	</div>
</div>	

Usare le liste

Se si considerano le slide come una serie di elementi, allora possiamo modificare la nostra struttura HTML come segue:


<ul class="slider-wrapper"><!-- contenitore interno -->
		<li class="slide" id="slide-1">...</li><!-- slide -->
		<li class="slide" id="slide-2">...</li>
		<li class="slide" id="slide-3">...</li>
</ul>

Se le slide sono in un ordine preciso (ad esempio una presentazione), potete anche usare liste ordinate (<ol>).

Il codice CSS

Consideriamo la seguente struttura HTML:


<div class="slider" id="main-slider"><!-- contenitore esterno -->
	<div class="slider-wrapper"><!-- contenitore interno -->
		<img src="image1.jpg" alt="Descrizione" class="slide" /><!-- slide -->
		<img src="image2.jpg" alt="Descrizione" class="slide" />
		<img src="image3.jpg" alt="Descrizione" class="slide" />
	</div>
	<div class="slider-nav"><!-- "Precedente" e "Successivo"  -->
		<button type="button" class="slider-previous">Precedente</button>
		<button type="button" class="slider-next">Successivo</button>
	</div>
</div>

Il movimento avrà luogo da destra verso sinistra. Ciò significa che il contenitore esterno avrà una data larghezza mentre il contenitore interno sarà più largo per contenere tutte le slide. Solo la prima slide sarà visibile inizialmente: per far ciò usiamo la proprietà CSS overflow.


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

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

Il contenitore interno ha i seguenti stili importanti:

  • Una larghezza tale da contenere tutte le slide.
  • Un'altezza stabilita per contenere le slide che saranno flottate.
  • Il posizionamento relativo per creare il movimento da destra verso sinistra.
  • Una transizione CSS sulla proprietà left che creerà il movimento. Potete usare anche una traslazione con le trasformazioni CSS. Se usate jQuery potete anche non usare questa tecnica CSS.

Le slide sono flottate e posizionate in modo relativo. Il posizionamento relativo in questo caso serve a JavaScript per calcolare esattamente l'offset di ciascuna slide.


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

La larghezza del contenitore interno verrà sovrascritta da JavaScript: 9999 pixels può anche non bastare se lo slideshow è a pieno schermo e ci sono molte slide.

Per applicare gli stili ai bottoni "Precedente" e "Successivo" dobbiamo prima resettare i loro stili predefiniti:


.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);
}

Per i link della paginazione possiamo invece usare i seguenti stili:


.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;
}

La classe current verrà applicata dinamicamente da JavaScript quando viene selezionata una slide.

Questo tipo di layout è comune quando l'effetto voluto è uno scorrimento. Se invece vogliamo un effetto di assolvenza e dissolvenza dobbiamo usare il posizionamento contestuale per creare uno stack di slide:


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

.slider-wrapper {
	width: 100%; /* La larghezza ora è uguale a quella del contenitore esterno */
	height: 683px;
	position: relative; /* Crea il contesto per il posizionamento assoluto */
}

.slide {
	position: absolute; /* Tutte le slide sono posizionate in modo assoluto */
	width: 100%;
	height: 100%;
	opacity: 0; /* Le slide sono nascoste */
	transition: opacity 500ms linear; /* Transizione su opacity */
}

/* Solo la prima slide è visibile inizialmente */

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

Usiamo opacity perchè display: none sopprime la lettura del contenuto negli screen reader (consultate CSS in Action: Invisible Content Just for Screen Reader Users di WebAIM per maggiori dettagli ).

Il posizionamento contestuale da noi creato segue l'ordine imposto dallo stacking: l'ultima slide sarà la prima ad essere visualizzata. Dato che non vogliamo questo effetto, nascondiamo tutte le slide tranne la prima.

JavaScript cambierà il valore di opacity sulla slide corrente e resetterà tale valore su 0 su tutte le altre. Se usate jQuery potete anche usare direttamente il metodo .animate().

Compatibilità con IE9

IE9 non supporta le transizioni CSS. Se vogliamo mantenere la compatibilità con questo browser dovremmo usare jQuery.

Per una panoramica sulle possibili soluzioni, consultate questo thread su Stack Overflow.

Il codice JavaScript

Slideshow senza paginazione

Gli slideshow senza paginazione usano solo i bottoni "Precedente" e "Successivo". Questi bottoni servono per incrementare e decrementare un contatore interno. Il contatore non è altro che un indice numerico usato per selezionare le slide. Inizialmente è impostato su 0 e il suo funzionamento è identico a quello degli indici degli array.

Così cliccando una prima volta sul pulsante "Successivo" il contatore avrà il valore di 1 e selezioneremo la seconda slide (la numerazione negli array comincia da 0). Quindi cliccando sul pulsante "Precedente" il contatore avrà valore 0 e selezioneremo di nuovo la prima slide. E così via.

Con jQuery possiamo usare il metodo .eq() con il nostro contatore, ma in JavaScript l'approccio è diverso:


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

Slideshow.prototype = {
	init: function() {
		this.slides = this.el.querySelectorAll( ".slide" ); // Una NodeList è simile ad un array
		//...
	},
	_slideTo: function( pointer ) {
		var currentSlide = this.slides[pointer]; // Slide corrente
		//...
	}
};

Una NodeList usa gli indici come un vero array. Un altro modo è quello di usare i selettori CSS3:


Slideshow.prototype = {
	init: function() {
		//...
	},
	_slideTo: function( pointer ) {
	
        var n = pointer + 1; // :nth-child() conta a partire da 1, non da 0	
		var currentSlide = this.el.querySelector( ".slide:nth-child(" + n + ")" ); // Slide corrente
		//...
	}
};

Il selettore :nth-child() conta a partire da 1 e non da 0, quindi dobbiamo incrementare il contatore di 1 o più semplicemente impostare il suo valore iniziale su 1 per selezionare le slide.

Una volta selezionata una slide la usiamo per far scorrere il contenitore interno da destra a sinistra. In jQuery usiamo il metodo .animate() con un valore negativo per la proprietà left del contenitore interno. Il valore negativo è ricavato dall'offset della slide selezionata:


(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 );

In JavaScript non esiste un metodo nativo .animate(), quindi dobbiamo usare le transizioni CSS:


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

Quindi modifichiamo il valore della proprietà left usando l'oggetto style:


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";
	}
};

Ora dobbiamo legare un evento click a ciascun pulsante. In jQuery usiamo il metodo .on() mentre in JavaScript usiamo il metodo addEventListener().

Dobbiamo anche verificare che il nostro contatore non abbia raggiunto un valore specifico, ossia 0 per il pulsante "Precedente" e il numero totale di slide per il pulsante "Successivo". In entrambi i casi i pulsanti devono essere mostrati o nascosti ed il contatore resettato sul valore corretto.


(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 ) { // Numero totale 
					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 JavaScript invece il codice sarà come segue:


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);
	}

};

Esempi

Slideshow con paginazione

Negli slideshow con paginazione ciascun link della paginazione corrisponde ad una singola slide. Non c'è più bisogno di avere un contatore interno poiché ciascun link contiene un'ancora che punta all'ID della slide corrispondente.

Le animazioni non cambiano. In jQuery abbiamo il seguente codice:


(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 JavaScript avremo:


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 = "";
			}
		}
	}
};

Da IE10 in poi potete manipolare le classi CSS con l'oggetto classList:


link.classList.add( "current" );

Da IE11 in poi potete gestire gli attributi di dati con la proprietà dataset:


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

Esempi

Slideshow con paginazione e pulsanti di controllo

Questo tipo di slideshow è particolare: dobbiamo combinare il contatore interno con le ancore dei link della paginazione. In altre parole quando clicchiamo sul terzo link della paginazione il contatore dovrebbe essere impostato su 2 e quando clicchiamo sul pulsante "Precedente" il contatore dovrebbe avere come valore 1. Quindi si tratta di sincronizzare i link della paginazione con i pulsanti "Precedente" e "Successivo".

Possiamo usare l'indice numerico degli elementi del DOM: quando clicchiamo sul terzo link della paginazione l'indice sarà 2 e lo useremo per impostare il nostro contatore. Inoltre su ogni link della paginazione dovremo effettuare gli stessi controlli che abbiamo implementato per i pulsanti "Precedente" e "Successivo".

In jQuery avremo questo codice:


(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(); // Indice numerico del DOM
					$.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 );

Il nostro contatore è stato dichiarato come proprietà dell'oggetto slideshow: in questo modo evitiamo di incorrere nei problemi di scope creati indirettamente dai metodi di callback di jQuery. Così il contatore è accessibile sia dentro che fuori dal namespace del nostro plugin.

Per ottenere l'indice di ciascun link della paginazione usiamo il metodo .index() di jQuery. Quindi impostiamo il nostro contatore sul valore ottenuto.

In JavaScript non c'è un metodo .index(), quindi la soluzione più rapida è usare gli attributi di dati con un loop for:


(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 );
				// Oppure: pagLink.dataset.index = k;
			}	
	    },
	    //...
    };
})();

Quindi possiamo scrivere:


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 );
			// Oppure: 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);
		}
}

Esempi

Conclusione

Gli slideshow rappresentano un'eccellente opportunità di migliorare l'interfaccia utente, ma senza la conoscenza delle basi del loro sviluppo non è possibile progettare alcun tipo di implementazione.

Esempi completi

JavaScript Slideshows