Dans ce tutoriel, nous allons apprendre à créer un élément qui reste affiché à l'écran en permanence même lorsqu'on scroll (barre de défilement) en "suivant" le défilement de l'écran.
L'intérêt est d'attirer l'attention tout en gardant cet élément en dehors du contenu principal de la page, on peut par exemple l'utiliser pour afficher un lien vers les réseaux sociaux : Facebook "like", Twitter, Google +1...
Cette technique est notamment utilisée sur l'Apple Store, pour afficher le panier d'achats en permanence - ex: http://store.apple.com/fr/browse/home/shop_ipad/family/ipad/select_ipad.
Pour réaliser cet effet, on va utiliser le positionnement CSS "fixed" ainsi que jQuery.
Voilà les sources du résultat final pour les gens pressés :
Étape 1 : structure HTML + CSS
Avant tout, on va partir sur avec cette page de base :
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style type="text/css"> #page{ position:relative; margin:50px auto; width:250px; } </style> </head> <body> <div id="page"> <div> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> </div> <!-- Fusée --> </div> </body> </html>
Comme vous le voyez, il y a une div #page dont le rôle est de contenir tous les éléments de la page dans une bande verticale de largeur fixe (250px).
Mais cette div porte également une propriété CSS position:relative
. Cela ne modifie en rien le positionnement de la div #page, mais cette propriété est très importante pour le positionnement de tous les éléments qu'elle contient.
En effet, grâce à cette propriété, tous les éléments contenus dans #page ayant un positionnement relative
ou absolute
seront positionnés par rapport à la div #page, et pas par rapport à toute la fenêtre.
Note: la valeur par défaut de l'attribut position est : static
.
Intéressons-nous maintenant au code HTML de la fusée :
<!-- Début fusée --> <div id="rocket_dummy"> <div id="rocket_dock"></div> <div id="rocket_mobile"> <div class="fire top"></div> <div class="fire bottom"></div> <div class="rocket_body"></div> </div> </div> <!-- Fin fusée -->
Et le code CSS qui va avec :
#rocket_dummy{ position:absolute; left:-100px; top:20px; width:55px; height:55px; background:blue; } #rocket_mobile{ position:absolute; top:0; margin:85px 0 0 -4px; width:60px; height:185px; background:rgba(255,0,0,0.5); } #rocket_mobile.fixed{position:fixed; top:0;} #rocket_dock{ position:absolute; width:28px; height:28px; top:164px; left:12px; background:#555; } #rocket_mobile .rocket_body{ position:absolute; width:60px; height:60px; top:63px; left:0; background:rgba(246,168,0,0.9); } #rocket_mobile .fire{ position:absolute; width:35px; height:74px; background:rgba(100,245,0,0.7); } #rocket_mobile .fire.top{ left:13px; top:7px; } #rocket_mobile .fire.bottom{ left:13px; top:104px; }
Pour faciliter les explications j'ai appliqué aux différents éléments des couleurs flashy.
Premier élément à regarder : #rocket_dummy
. Dans la version finale cette div n'est pas visible, et pourtant c'est une pièce maîtresse du système , puisqu'elle permet de positionner la fusée.
En effet, cette div est positionnée en absolute
(par rapport à #page), et tous les autres éléments de la fusée sont positionnés par rapport à #rocket_dummy
.
#rocket_mobile
est utilisé comme conteneur de la fusée, c'est cet élément et tout son contenu qui va suivre le défilement.
Ensuite il reste .rocket_body
qui correspond au corps de la fusée (le rond noir), et aussi les 2 flammes en haut et en bas : .fire.top
et .fire.bottom
. Rien de bien compliqué concernant ces 3 éléments, ils sont simplement positionnés en absolute (par rapport à leur parent : #rocket_mobile
).
Étape 2 : images d'arrière-plan
Maintenant on va remplacer toutes ces couleurs flashy par des images d'arrière-plan en CSS.
Pour cela, je vais vous fournir l'image qui contient l'arrière plan des différents éléments de la fusée, téléchargez cette image :
Comme vous pouvez le voir, il n'y a qu'une seule image qui regroupe tous les éléments, au lieu d'avoir une image par élément.
C'est ce qu'on appelle un sprite CSS. CSS permet de positionner les images d'arrière-plan, de la "décaller" pour afficher la zone souhaitée.
Cette technique offre plusieurs avantages :
- elle limite le nombre d'images à charger, et donc le nombre de requêtes HTTP envoyées au serveur
- elle accélère aussi le chargement des pages
Voici la syntaxe de la propriété CSS qui permet de définir une image d'arrière plan et de la positionner :
background: url('rocket-sprite.png') -60px 10px no-repeat;
La partie en rouge permet d'indiquer le chemin de l'image. Vous pouvez soit y mettre l'URL absolue (http://.../rocket-sprite.png) soit le chemin relatif par rapport au fichier qui contient le CSS. En ce qui me concerne j'ai placé l'image dans le même dossier que ma page html.
La partie en bleu correspond à la position de l'image d'arrière plan : le premier chiffre correspond au décalage horizontal, et le second au décalage vertical. La seule "difficulté" est que les axes sont inversés : l'origine (0px 0px) correspond au coin en haut à gauche de l'image.
Le no-repeat à la fin indique que l'image ne doit pas être répétée comme une mosaïque lorsqu'elle n'est pas assez grande pour couvrir tout l'arrière plan de l'élément. Ici ça n'est pas le cas mais il faut quand même donner une valeur pour cette propriété.
Voici donc les cotes des différentes parties de l'image, elles vont nous être très utiles pour positionner les différents background :
Prenons l'exemple de .rocket_body
, la partie de l'image qui nous intéresse est le rond noir. Cette partie est un carré de 60px
de côté. De plus, on sait qu'il y a une bande horizontale de 28px en haut qui ne nous intéresse pas, on va donc devoir décaler l'image de 28px vers le haut, et comme les axes sont inversés on écrit -28px
:
.rocket_body{
width:60px; height:60px;
background: url('rocket-sprite.png') -28px 0px no-repeat;
}
Bien, maintenant que vous connaissez la technique, je ne vais pas vous laisser positionner tous les éléments un par un, je vais vous fournir la nouvelle version du CSS ça ira plus vite ^^ :
#rocket_dummy{ position:absolute; left:-100px; top:20px; width:55px; height:55px; } #rocket_mobile{ position:absolute; top:0; margin:85px 0 0 -4px; width:60px; height:185px; } #rocket_mobile.fixed{position:fixed; top:0;} #rocket_dock{ position:absolute; width:28px; height:28px; top:164px; left:12px; background:url('rocket-sprite.png') 0 0 no-repeat; } #rocket_mobile .rocket_body{ position:absolute; width:60px; height:60px; top:63px; left:0; background:url('rocket-sprite.png') 0 -28px no-repeat; } #rocket_mobile .fire{ position:absolute; width:35px; height:78px; } #rocket_mobile .fire.top{ left:13px; top:3px; background:url('rocket-sprite.png') -60px 0 no-repeat; } #rocket_mobile .fire.top.on{background-position:-95px 0;} #rocket_mobile .fire.bottom{ left:13px; top:104px; background:url('rocket-sprite.png') -60px -78px no-repeat; } #rocket_mobile .fire.bottom.on{background-position:-95px -78px;}
Vous remarquez au passage que j'ai ajouté une règle qui modifie la position de l'arrière plan pour les éléments qui ont la classe 'fire' et la classe 'on'. Ça permet tout simplement d'afficher ou de masquer la flamme !
Voici le résultat :
Étape 3 : suivre le scroll avec position:fixed
En en a terminé avec la partie HTML et CSS, maintenant il reste le plus difficile : le suivi du scroll en javascript, à l'aide de jQuery.
Tout l'enjeu va être de jouer sur le positionnement de #rocket_mobile
en grés des variations du défilement. Regardons les propriétés CSS de cet élément de plus près :
#rocket_mobile{ position:absolute; top:0; margin:85px 0 0 -4px; width:60px; height:185px; } #rocket_mobile.fixed{position:fixed; top:0;}
Par défaut, #rocket_mobile
est positionné en absolute
par rapport à son parent : #rocket_dummy
. Pour le moment il disparait lorsqu'on scroll.
Mais il y a moyen de faire en sorte qu'il soit affiché en permanence, en utilisation la propriété position:fixed
.
Cet attribut est un peu particulier, puisqu'il positionne l'élément par rapport à la fenêtre du navigateur, et pas par rapport au document. Et il se trouve que lorsqu'on scroll c'est le document qui défile, mais la fenêtre du navigateur, elle, ne bouge absolument pas. Du coup on se retrouve avec notre élément qui "flotte" au dessus du document et qui reste toujours au même endroit, il est fixe.
L'idée c'est donc d'attribuer ou de retirer position:fixed
en fonction du scroll :
- lorsqu'on est en haut de la page, on reste en position absolute
- dès qu'on scroll suffisamment pour que
#rocket_mobile
s'apprête à disparaitre en haut de l'écran, on le passe enposition:fixed
pour figer sa position
Voici le code javascript qui permet de gérer ce changement de position :
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> <script type="text/javascript"> /** * Animation de la fusée avec Javascript **/ jQuery(document).ready(function($){ // Stockage des références des différents éléments dans des variables rocket = $('#rocket_mobile'); firetop = $('#rocket_mobile .fire.top'); firebottom = $('#rocket_mobile .fire.bottom'); // Calcul de la marge entre le haut du document et #rocket_mobile fixedLimit = rocket.offset().top - parseFloat(rocket.css('marginTop').replace(/auto/,0)); // On déclenche un événement scroll pour mettre à jour le positionnement au chargement de la page $(window).trigger('scroll'); $(window).scroll(function(event){ // Valeur de défilement lors du chargement de la page windowScroll = $(window).scrollTop(); // Mise à jour du positionnement en fonction du scroll if( windowScroll >= fixedLimit ){ rocket.addClass('fixed'); } else { rocket.removeClass('fixed'); } }); }); </script>
Explication:
On commence par récupérer des références vers les éléments qu'on va manipuler dans des variables. Vous vous demandez peut-être pourquoi faire ça, on pourrait simplement utiliser le sélecteur jQuery quand on souhaite manipuler ces éléments...
Vous avez raison, mais comme on va très souvent y accéder, c'est mieux de les sélectionner une seule fois au chargement de la page et de les stocker plutôt que de les sélectionner à chaque fois, car c'est beaucoup plus rapide en terme de performance (et donc de fluidité de l'animation).
Ensuite on calcul le nombre de pixels qui sépare le haut de la page du haut de #rocket_mobile
. Pour cela on utilise la fonction jQuery .offset(), qui retourne la position de l'élément par rapport au document. Seulement, cette méthode offset ne prend pas on compte les marges (margin et padding) de l'élément, donc on retire cette marge, que l'on récupère via parseFloat(rocket.css('marginTop').replace(/auto/,0))
.
Enfin, on défini une fonction qui va être exécutée à chaque fois qu'on scroll, dont le rôle et d'ajouter ou de retirer la classe .fixed (et donc le position:fixed
) à #rocket_mobile
en fonction du défilement.
Étape 4 : animation des moteurs
Dans cette étape on va gérer l'affichage et des flammes jaunes qui s'allument lorsqu'on scroll.
On ne doit afficher qu'une seule flamme en fonction du sens du scroll :
- lorsqu'on scroll vers le bas on affiche la flamme du haut
- lorsqu'on scroll vers le haut on affiche la flamme du bas
Et ça va un peu se corser, parce que jQuery ne permet pas de connaître le sens du scroll. Du coup il va falloir trouver une astuce pour avoir cette information.
Pour ça, on va créer une nouvelle variable LAST_SCROLL_OFFSET qui va être mises à jour à chaque événement scroll. Du coup, il suffit de comparer la valeur de LAST_SCROLL_OFFSET avec la valeur actuelle du scroll (donnée par $(window).scrollTop();
) :
- si le scroll a augmenté, cela signifie qu'on a scrollé vers le bas
- sinon, c'est qu'on a scrollé vers le haut
jQuery(document).ready(function($){ // Stockage des références des différents éléments dans des variables rocket = $('#rocket_mobile'); firetop = $('#rocket_mobile .fire.top'); firebottom = $('#rocket_mobile .fire.bottom'); LAST_SCROLL_OFFSET = $(window).scrollTop(); // Calcul de la marge entre le haut du document et #rocket_mobile fixedLimit = rocket.offset().top - parseFloat(rocket.css('marginTop').replace(/auto/,0)); // On déclenche un événement scroll pour mettre à jour le positionnement au chargement de la page $(window).trigger('scroll'); $(window).scroll(function(event){ // Valeur de défilement lors du chargement de la page windowScroll = $(window).scrollTop(); // Mise à jour du positionnement en fonction du scroll if( windowScroll >= fixedLimit ){ rocket.addClass('fixed'); } else { rocket.removeClass('fixed'); } // Animation flammes // Allumage if( rocket.hasClass('fixed') ){ if( windowScroll > LAST_SCROLL_OFFSET ){ // DOWN firetop.addClass('on'); firebottom.removeClass('on'); } else { // UP firetop.removeClass('on'); firebottom.addClass('on'); } } // Mise à jour LAST_SCROLL_OFFSET = windowScroll; }); });
Allumer le feu c'est bien, mais il faut aussi pouvoir l'éteindre dès qu'on arrête de scroller. Or il n'existe pas d'événement "scrollstop" en JavaScript, donc il va falloir une fois de plus improviser.
Le plus facile est de déclencher une fonction après chaque scroll avec un petit délai (de 70 millisecondes). Le rôle de cette fonction va tout simplement être d'éteindre les moteurs (en supprimant la classe 'on').
Mais attention, il y a une autre condition :pour éteindre les moteurs il faut que le scroll soit terminé. La seule façon de le savoir c'est de mémoriser l'heure précise à chaque événement scroll. À partir de là il ne reste plus qu'à vérifier que le dernier scroll est suffisamment "ancien" pour éteindre les moteurs.
Voilà le javascript final :
jQuery(document).ready(function($){ // Stockage des références des différents éléments dans des variables rocket = $('#rocket_mobile'); firetop = $('#rocket_mobile .fire.top'); firebottom = $('#rocket_mobile .fire.bottom'); LAST_SCROLL_OFFSET = $(window).scrollTop(); LAST_SCROLL_TIME = new Date().getTime(); // Calcul de la marge entre le haut du document et #rocket_mobile fixedLimit = rocket.offset().top - parseFloat(rocket.css('marginTop').replace(/auto/,0)); // On déclenche un événement scroll pour mettre à jour le positionnement au chargement de la page $(window).trigger('scroll'); $(window).scroll(function(event){ // Valeur de défilement lors du chargement de la page windowScroll = $(window).scrollTop(); // Mise à jour du positionnement en fonction du scroll if( windowScroll >= fixedLimit ){ rocket.addClass('fixed'); } else { rocket.removeClass('fixed'); } // Animation flammes // Allumage if( rocket.hasClass('fixed') ){ if( windowScroll > LAST_SCROLL_OFFSET ){ // DOWN firetop.addClass('on'); firebottom.removeClass('on'); } else { // UP firetop.removeClass('on'); firebottom.addClass('on'); } } // Arrêt setTimeout(function(){ if( new Date().getTime() - LAST_SCROLL_TIME > 50 ){ firetop.removeClass('on'); firebottom.removeClass('on'); } },70); // Mise à jour variables LAST_SCROLL_OFFSET = windowScroll; LAST_SCROLL_TIME = new Date().getTime(); }); });
Ce tutoriel jQuery est maintenant terminé. Dans le premier exemple il y a un petit effet d'inertie lorsque la fusée s'arrête, je n'ai pas expliqué ce point dans cette formation mais sachez que ce "bonus" est fourni dans le projet que vous pouvez télécharger sur stockmotion :
Ce projet contient :
- Les sources HTML/CSS/Javascript de ce tutoriel
- En bonus une version spéciale avec un effet d'inertie
- Le fichier Photoshop du sprite CSS, vous permettant de modifier les couleurs ou la forme de la fusée
Allez donc jeter un oeil sur cette page : démon d'halloween.
PS : Faut suppr mon com précedent, merci :)
merci pour ce code très intéressant, seulement j'essaye de l'appliquer sur des éléments (paniers ou blocs d'options recherche) qui ont une hauteur de 500px par ex, donc quand je scroll en bas de la page, les roquettes rentrent en collision avec le footer !
comment peut on faire pour demander qu'il s'arrête à une distance x du bas de la page ? j'ai essayé mais suis novice en JS ! merci d'avance
Merci pour le tuto. Sur votre site il y a un bandeau en pied de navigateur qui reste fixe (<div id="excel_forum_promo">). Cette effet correspond tout à fait à ce que je souhaite avoir sur mon site à savoir ; mon footer toujours visible quelque soit la longueur de la page et l'état du scroll.
Est-ce que vous pouvez me dire comment vous l'avez réalisé?
Merci par avance.
Très simplement en utilisation la propriété CSS postiion:fixed :
Merci d'avance