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 là.
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.