Scroll horizontal en jQuery sur un site web

Tous les navigateurs web sont conçus pour défiler vers le bas lorsqu’on scrolle avec la souris. Créer un site internet à scroll horizontal n’est donc pas évident, il va falloir détourner le fonctionnement normal du navigateur avec un script JS (ou jQuery). Cet article vous explique comment créer un scroll horizontal en jQuery pour un site web.

Et d’abord, pourquoi faire un scroll horizontal ?

Ah ben c’est comme vous voulez, c’est pas obligé bien entendu. Les goûts et les couleurs hein…

Le client qui nous l’a demandé en a fait une marque de fabrique. Il s’agit de Florès et de sa filiale Tool to Team : assistance maîtrise d’ouvrage, économie de la construction et aménagement d’espace.

Pour vous faire une idée voici une copie d’écran de leurs deux sites :

Le scroll horizontal donne l’impression qu’une histoire se déroule. Dans ces deux cas, ça fonctionne bien, je vous laisse le découvrir ici et .

Mais nous ne sommes pas là pour discuter de pourquoi, plutôt du comment.

Le cahier des charges d’un bon scroll horizontal en jQuery

On a voulu écrire un script qui permet une gestion complète de toutes les problématiques liées au scroll en général et au scroll horizontal en particulier :

  • Scroll « doux » sans à-coup
  • Un système d’inertie qui freine le scroll progressivement, sans arrêt brutal
  • Une limitation de la vitesse pour éviter un emballement notamment sur les trackpads des laptops
  • Une navigation possible au clavier et avec des boutons à cliquer
  • Un appel à l’action clair pour que les utilisateurs comprennent le truc rapidement

Mise en jambe avec un soupçon d’HTML

On va commencer par la structure HTML de la page que vous voulez passer en scroll horizontal. Cette structure utilise les classes Bootstrap pour le container principal.

<div class="container-fluid no-padding" id="main-container">
	<div class="zone_home home_navpoint" id="zone_home_principale">
	</div>

	<div class="zone_home home_navpoint" id="zone_home_2">
	</div>

	<div class="zone_home home_navpoint" id="zone_home_3">
	</div>
</div>

Dans cet exemple, il y a trois sections de classe zone_home. Ces sections sont en outre dotées d’une classe home_navpoint qui servira de point de repère de navigation dans le JavaScript. Chaque section possède en outre un ID qui permettra d’adapter les propriétés CSS au cas par cas. On peut ajouter autant de sections qu’on veut, du moment qu’on garde la même structure.

Une touche de CSS

Dans la suite, on va supposer que seule la page d’accueil du site doit être en scroll horizontal. Si vous voulez l’appliquer à une autre page, il faudra adapter ou ajouter les classes correspondantes.

Pour faire fonctionner cette structure en bonne intelligence avec le JS à venir, il faudra en outre ajouter ces quelques lignes dans votre CSS :


body.home{
	overflow: hidden;
	width: 20000px;/*la largeur du body doit être fixée à une valeur supérieure à la largeur maximum possible*/
}

body.home #main-container{
	margin: 0;
	height: 100%;/*le conteneur principal doit couvrir toute la hauteur du body*/
	padding: 0;
	width: auto;
}

#main-container{
	margin-top: 100px; /*on laisse la place pour le menu principal*/
	padding: 0;
}

.zone_home{
	height: 100%;
	display: block;
	float:left;/*toutes les zones sont flottantes*/
	position: relative;
	width : 100vw;/*toutes les zones ont par défaut la largeur de l'écran, ce comportement peut être modifié au cas par cas*/
}

#zone_home_principale{
	width: 100vw;
	height: 100%;
	overflow: hidden;
	background-size: cover;
	background-position: 0% 100%;
}

Toutes les zones sont flottantes, elles vont donc s’empiler les unes à côté des autres. L’inconvénient de cette méthode est qu’elles sortent du flux. Il faut en conséquence fixer une largeur provisoire au body. Plus tard, dans le JS, cette largeur sera calculée plus précisément mais en attendant, elle doit être définie clairement au début pour éviter les bugs d’affichage.

Voilà, la base est en place, on peut maintenant réfléchir au script principal.

Le scroll horizontal en jQuery : les fonctions secondaires

Bon, à partir de maintenant, si vous ne connaissez pas le JavaScript et le jQuery, je ne vais pas vous mentir, ça va être chaud.

Le script est découpé en plusieurs fonctions :

  • Déplacement horizontal en général
  • Scroll vers le navpoint suivant
  • Scroll vers le navpoint précédent
  • Navigation au clavier ou au clic sur un bouton
  • Mise en place de tous les élements sur la page d’accueil

Pour l’ensemble du script, on implémente une variable globale qui indiquera s’il y a déjà eu déplacement ou non :

var HomeMoved = false;

Déplacement horizontal

Ce rôle est assuré par la fonction scrollHomeMoveTo qui prend deux arguments : la destination « destinationX » et le point de départ « from » :

function scrollHomeMoveTo(destinationX,from){
	HomeMoved = true;//on enregistre qu'un mouvement a eu lieu
	$('#home_navigation').removeClass('bigger');

	//Distance à parcourir
	var distance = Math.abs(destinationX - from);
	var duration = distance;
	//on attribue arbitrairement la même valeur à la durée de déplacement : ça permet d'avoir une vitesse constante de 1px/ms quel que soit le déplacement.

	//Dans le cas d'une durée trop longue, celle-ci est plafonnée
	if(duration > 2000){
		duration = 2000;
	}

	$('html, body').animate({//lancement de l'animation de scroll
		scrollLeft: destinationX
	}, {
      queue: false,
      duration: duration
    });
}

Le script contient une animation qui déplace scroll du point de départ au point d’arrivée. Rien de très compliqué ici.

Déplacement vers le navpoint immédiatement à droite

Le script doit déplacer le point de scroll vers la droite en utilisant la fonction précédente :

function scrollHomeMoveForward(){
	var currentPosX = $('html,body').scrollLeft();//mesure de la position actuelle

	//Cherche le point immédiatement après la valeur currentPosX
	$('.home_navpoint').each(function(){
		if( 
			$(this).offset().left > currentPosX 
			&& $(this).offset().left - currentPosX > 200
		){
			destinationX = $(this).offset().left;
			return false;
		}
	});
	scrollHomeMoveTo(destinationX,currentPosX);
}

La fonction mesure la position courante du scroll puis la compare à celle de chaque nav_point qu’elle trouve. Si cette position courante est au-delà de la position du nav_point (avec une marge de 200px), sa position est enregistrée come destination et la fonction s’arrête pour ne pas aller chercher les nav_point suivants.

Un petit mot sur le $(‘html,body’) pour la mesure de position. C’est un problème de compatibilité navigateur : Firefox et Edge semblent utiliser le html alors que Chrome et Safari utilisent le body pour définir l’origine du scroll.

On recommence avec le déplacement vers le navpoint immédiatement à gauche

function scrollHomeMoveBackward(){
	var currentPosX = $('html,body').scrollLeft();

	//Cherche le point immédiatement avant la valeur currentPosX
	var maxX = 0;
	$('.home_navpoint').each(function(){
		var pointX = $(this).offset().left;
		if( pointX < currentPosX ){
		//si le navpoint est placé avant la position courante, on enregistre sa position dans maxX. On s'accorde une tolérance de 200px.
			var diff = currentPosX - pointX;
			if(pointX > maxX && diff > 200)
				maxX = pointX;
		}
	});
	scrollHomeMoveTo(maxX,currentPosX);
}

Le principe est similaire même si la syntaxe est un peu différente. On initialise une variable locale maxX qui servira à enregistrer la position une fois que le déplacement a eu lieu.

Navigation au bouton ou au clavier

Les fonctions que l’on vient d’écrire vont dans un premier temps être utilisées pour mettre en place une navigation par clic sur des boutons ou avec les flèches de clavier. Il ne faut pas oublier que le scroll horizontal est assez contre-intuitif et en terme d’UX, il faut prévoir d’assister l’utilisateur au maximum.

function initHomeNavKeyboard(){

	//En avant
	$('.btn_next').click(function(){ scrollHomeMoveForward(); })

	//En arrière
	$('.btn_prev').click(function(){ scrollHomeMoveBackward(); })

	$(document).keydown(function(e){
	    //Flèche droite ou bas
	    if (e.keyCode == 39 || e.keyCode == 40) { 
	    	scrollHomeMoveForward();
	    	return false;
	    }
	    //Flèche gauche ou haut
	    if (e.keyCode == 37 || e.keyCode == 38) { 
	    	scrollHomeMoveBackward();
	    	return false;
	    }
	});

	//Si après 7 secondes d'inactivité (variable HomeMoved)
        //agrandit les boutons de navigation
	setTimeout(function(){ 
		if(!HomeMoved)
			$('#home_navigation').addClass('bigger');
	}, 7000);
}

La fonction permet donc 4 actions :

  • bond en avant d’un nav_point en cliquant sur un bouton ayant la classe btn_next
  • retour en arrière d’un nav_point en cliquant sur un bouton ayant la classe btn_prev
  • bond en avant d’un nav_point en appuyant sur la flèche droite du clavier
  • retour en arrière d’un nav_point en appuyant sur la flèche gauche du clavier

Cerise sur le gâteau : si au bout de 7 secondes (une sorte d’éternité dans le web) l’utilisateur n’a bougé ni en avant, ni en arrière, on considère qu’il n’a pas compris le principe alors on ajoute la classe bigger sur les boutons de navigation pour par exemple les faire paraître plus gros (ou autre, à régler en CSS) . On réutilise pour cela la variable globale HomeMoved.

Le gros morceau : la fonction mère setHome

La fonction setHome va régir le fonctionnement global du scroll, en utilisant les fonctions précédentes. Je l’ai appelée comme ça puisque sur les deux sites où elle est utilisée, le scroll horizontal est réservé à la page d’accueil.

Elle devra bien entendu être appelée après le chargement des pages qui utilisent le scroll horizontal (page d’accueil dons notre exemple) :

//Fin du chargement de la page
$(window).on('load',function(){	

  if($('body.home').length){
		setHome();
  }
}

Voici donc la bête en question :

function setHome(){

	var smoothScroll = {
	    speed: 0,
	    delay: 10, // en ms
	    timer: null,
	    scrollSpeed: 4,
	    inertia: .95,
	    init: function(){
	        this.setEventsListeners();
	    },
	    setEventsListeners: function(){
	    	(function(self) {
	    		$(window).on('wheel', function(event){

				    self.setSpeed(event);
				    //on se souviendra qu'il y a eu mouvement, au cas où
				    HomeMoved = true;
				    //diminution de taille des boutons de navigation
				    $('#home_navigation').removeClass('bigger');
				});
			})(this);
	    },
	    setSpeed: function(e){
	    	//Limitation de vitesse : si speed ne dépasse pas 20, on accélère, sinon on ralentit
	    	if(Math.abs(this.speed) <= 20){
	    		this.speed += e.originalEvent.deltaY > 0 ? -this.scrollSpeed : this.scrollSpeed;
	    	}
                //Le timer sert à "glisser" entre deux positions de la molette de souris
	    	if(this.timer == null){
	    		this.timer = setTimeout(this.smoothScroll, this.delay, this); 
	    	}
	    	e.preventDefault();
	    },
	    smoothScroll: function(scope){

			var self = scope;
	    	self.speed *= self.inertia;

	    	//déclenche le scroll
	    	window.scrollTo(window.scrollX - self.speed, 0);

	    	if(self.speed < self.inertia && self.speed > -self.inertia){
	    		self.speed = 0;
	    		clearTimeout(self.timer);
	    		self.timer = null;
	    	}else{
	    		self.timer = setTimeout(self.smoothScroll, self.delay, self);
	    	}
	    }
	}


	// l'objet smoothScroll n'est initialisé que sur PC, la variable isMobile devant être définie par ailleurs
	if(!isMobile){
		smoothScroll.init();
	}


	//Navigation fleches clavier ou bouton fleches sur PC
	if(!isMobile){
		initHomeNavKeyboard();
	}


	////////ajustements dynamique du CSS////////////

	footerHeight = $("footer").height();

	//Hauteur du container
	$('#main-container').css('height',(windowHeight - footerHeight)+'px');

	//Réglage des largeur des div plein écran
	$('.zone_home_fullscreen').each(function(){
		$(this).css('width',windowWidth+'px');
	});

	//Calcul de la largeur totale des zones home pour container
	var largeurContainer = 0;
	$('.zone_home').each(function(){
		largeurContainer += getRealWidth($(this));
	})

	$('body').css('width',(largeurContainer + 10)+'px');

}

J’y comprends que dalle

Albert Einstein

On commence par définir un certain nombre de paramètres et de méthodes au sein de l’objet smoothScroll :

Les paramètres

  • speed définit la vitesse (elle est au début initialisée à zéro)
  • delay définit la durée entre deux avancée du scroll
  • timer est l’amortisseur qui va permettre d’obtenir un scroll « doux » et pas saccadé
  • scrollSpeed est une valeur arbitraire qui définit la vitesse de scroll
  • inertia est un coefficient qui définit l’inertie du scroll pour éviter qu’il ne s’arrête d’un seul coup

Les méthodes

  • init() : initialise la fonction lorsqu’elle est appelée
  • setEventListeners() : lance le scroll dès que la molette de la souris est activée
  • setSpeed() : calcule la vitesse du scroll avec un système de limitation, utile notamment sur les trackpads des laptops
  • smoothScroll() : règle l’inertie du scroll en utilisant le paramètre inertia. Si la vitesse absolue est inférieure à l’inertie, elle est ramenée à zéro pour que le scroll s’arrête. Sinon le scroll continue en utilisant le timer, aussi longtemps que la molette de al souris est activée

Ajustements du CSS

La dernière partie regroupe les modifications dynamiques faites au CSS. Rien de très difficile si ce n’est la fonction getRealWidth() qui calcule la largeur d’un élément en tenant compte des éventuels padding :

function getRealWidth(obj){
  var largeur = obj.width() + parseInt(obj.css('paddingLeft')) + parseInt(obj.css('paddingRight'));
  return largeur;
}

Conclusion

Ce script est naturellement à adapter en fonction de la situation. En particulier, il faudra rajouter les boutons de navigation d’ID home_navigation et vous pourrez faire dire ce que vous voulez à la classe « bigger ». N’oubliez pas de créer la variable isMobile pour détecter si l’utilisateur est sur mobile ou sur PC.

Références