Plug-in scrollDirection disponible sur GitHub: https://github.com/franklang/detect-scroll-direction.
Les bases de la mise en place
Le composant Sticky de Foundation 6 for Sites permet de fixer des éléments dans la fenêtre. La technique est utilisée par de nombreux sites pour, par exemple, laisser le bandeau affiché lorsque l’utilisateur scrolle vers le bas de la page.
Il manque cependant une fonctionnalité très ergonomique, notamment sur les plus petits devices (smartphones, petites tablettes) qui consiste à masquer le bandeau lorsque l’utilisateur scrolle vers le bas de la page et à l’afficher dès qu’il scrolle vers le haut (afin qu’il n’ait pas besoin de scroller jusqu’au haut de page pour atteindre le menu, par exemple). Et cette seconde technique est présente sur de plus en plus de sites (https://wpformation.com/ – visionner avec un Smartphone, http://www.eurosport.fr/cyclisme/ – visionner avec un Smartphone).
On commence par mettre en place le composant Sticky (jusque là, pas de dev spé : tout est disponible dans Foundation.
Ensuite, on ajoute en spé (via jQuery) un data-attribute
nommé data-scroll-direction
sur l’élément HTML
de la page. La valeur de ce data-attribute
sera down
ou up
selon que l’utilisateur scrolle vers le bas ou vers le haut de la page.
|
/* * Detect scroll direction (up/down) */ var scrollDir = 0; $(window).scroll(function () { var curScrollDir = $(this).scrollTop(); if (curScrollDir > scrollDir) { /* Scrolling Down */ $('html').attr('data-scroll-direction', 'down'); } else { /* Scrolling Up */ $('html').attr('data-scroll-direction', 'up'); } scrollDir = curScrollDir; }); |
On cible ensuite les éléments à masquer au scroll down
.
|
// On scroll down, hide page header 1st row [data-scroll-direction="down"] { .header { &-wrapper { &.sticky { .header { &-left-col, &-right-col { display: none; } } } } } } |
Illustration:
1. L’utilisateur arrive sur la page; elle se présente comme ceci:

2. L’utilisateur a scrollé vers le bas. Dans notre exemple, une partie seulement du bandeau est masquée. On laisse en Sticky le moteur de recherche car il est très important sur ce site.

3. L’utilisateur a scrollé vers le haut (1px de scroll vers le haut suffit). La partie masquée du bandeau réapparait.

Appliquer une animation et un délais
Utilisation du projet animate.css
Importer le projet https://daneden.github.io/animate.css/ dans votre propre projet (j’utilise npm pour celà). On charge uniquement les animations dont on a besoin:
|
@import '~animate.css/source/sliding_exits/slideOutUp.css'; @import '~animate.css/source/sliding_entrances/slideInDown.css'; |
Agir sur votre élément Sticky en invoquant les animations et en ajoutant un temps d’exécution:
|
[data-scroll-direction="down"] { .page-header.sticky { animation: slideOutUp 0.3s; } } [data-scroll-direction="up"] { .page-header.sticky { animation: slideInDown 0.3s; } } |
La fonction hideWithDelay
Permet d’appliquer une classe .hide
(qui n’est autre qu’un display: none;
) issue de Foundation 6 for Sites au bout de x secondes, qui correspond au temps qu’il faut à votre animation CSS pour se terminer. Dans notre exemple, l’animation CSS mettra 300ms à s’exécuter mais on ajoute la classe .hide
au bout de 290ms pour éviter un effet de flash à l’écran:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
var scrollDir = 0, $_HTML = $('html'), $_PAGE_HEADER = $('#pageHeader').find('.page-header'); function hideWithDelay(scrollDirection, delay) { setTimeout(function(){ if ( scrollDirection === 'down' ) { $_PAGE_HEADER.addClass('hide'); } else { $_PAGE_HEADER.removeClass('hide'); } }, delay); } $(window).scroll(function () { var curScrollDir = $(this).scrollTop(); if (curScrollDir > scrollDir) { /* Scrolling Down */ $_HTML.attr('data-scroll-direction', 'down'); hideWithDelay('down', 290); // ici, le délais doit être un peu plus réduit que celui déclaré dans la feuille de style pour l'animation } else { /* Scrolling Up */ $_HTML.attr('data-scroll-direction', 'up'); hideWithDelay('up', 290); } scrollDir = curScrollDir; }); |
Résolution d’erreurs, bugs…
Des éléments de la page ou l’ascenseur de la fenêtre se mettent à clignoter (flickering, blinking)
Source: sticky header blinking.
Testé et fonctionnel sur un projet en prod: il suffit de mettre une hauteur fixe à l’élément sticky. Si votre site/appli est Responsive, que la hauteur est différente d’un breakpoint à l’autre et que vous avez la chance d’utiliser Foundation For Sites (v6 dans mon exemple) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
// FIX avoid element/scrollbar flickering function avoidFlickering(stickyElementID) { var $_STICKY_ELEMENT = $('#'+stickyElementID); $_STICKY_ELEMENT.removeAttr('style'); var STICKY_ELEMENT_DIMENSIONS = Foundation.Box.GetDimensions(document.getElementById(stickyElementID)), STICKY_ELEMENT_HEIGHT = STICKY_ELEMENT_DIMENSIONS.height; $_STICKY_ELEMENT.css('height', STICKY_ELEMENT_HEIGHT); } $(document).ready(function() { avoidFlickering('pageHeader'); }); $(window).on('changed.zf.mediaquery', function(event, newSize, oldSize) { avoidFlickering('pageHeader'); }); |
Même solution en vanilla JS (mais avec Lodash)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
/* * Detect scroll direction (up/down) */ const DIRECTION_UP = 'up', DIRECTION_DOWN = 'down'; let scrollDir = 0, isHidden = false, $_WINDOW = $(window), $_HTML = $('html'), $_PAGE_HEADER = $('#pageHeader').find('.page-header'); const debounce = require('lodash.debounce'); function toggleHeaderDisplay(scrollDirection) { if (scrollDirection === DIRECTION_DOWN && !isHidden) { $_PAGE_HEADER.addClass('hide'); isHidden = true; } else if (scrollDirection === DIRECTION_UP && isHidden) { $_PAGE_HEADER.removeClass('hide'); isHidden = false; } } if ($_PAGE_HEADER) { const scroll = function () { const computeHeader = $_PAGE_HEADER.hasClass('sticky'); const curScrollDir = $_WINDOW.scrollTop(); if (curScrollDir < 0) { // -- iPhone/Safari allows to go "above" page top, do not treat this case return; } const direction = (curScrollDir > scrollDir) ? DIRECTION_DOWN : DIRECTION_UP; const curDir = $_HTML.attr('data-scroll-direction'); if (direction !== curDir) { $_HTML.attr('data-scroll-direction', direction); } if (computeHeader) { toggleHeaderDisplay(direction); } scrollDir = curScrollDir; }; const debouncedScroll = debounce(scroll, 25); $(window).scroll(debouncedScroll); } |
Cas particulier: les éléments sticky disparaissent totalement en haut de l’écran (avec effet d’animation) avant de réapparaître brutalement
ATTENTION: on part bien du composant Sticky de Foundation for Sites v6, animé via la bibliothèque animate.css (et plus précisément l’animation slideOutUp
). Ce cas particulier peut éventuellement vous donner des pistes si vous utilisez un autre composant sticky et une autre bibliothèque d’animation, mais il existe aujourd’hui d’autres moyens de produire un effet de smooth scroll qu’il vaut mieux privilégier.
Pour ce qui est de la qualité de cette soultion, ne la cherchez pas… Mais elle a le mérite de fonctionner!
Dans ce cas là, les animations proposées par la bibliothèque Animate.css peuvent ne pas convenir. Dans la déclaration CSS transform: translate3d(0, -100%, 0);
la valeur de -100%
correspond à la hauteur de l’élément. C’est ce fait qu’on voit l’élément sticky scroller entièrement hors-champ avant de réapparaître brutalement.
Mon idée: appliquer temporairement (le temps de l’animation) un margin-top
de la hauteur de l’élément à ce dernier afin qu’il s’arrête aux limites de la fenêtre, puis retirer ce style.
Ressources en ligne:
Nouvelle version:
Ma nouvelle animation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
$slide-up-common-duration: 0.3s; // Custom animations for .page-header, .page-leftColumn and .page-searchBar "smooth" sticky @keyframes slideOutUp_pageHeader { 0% { transform: translate3d(0, 0, 0); } 99.999999% { transform: translate3d(0, -100%, 0); } 100% { visibility: hidden; } } .slideOutUp_pageHeader { animation-name: slideOutUp_pageHeader; } [data-scroll-direction="down"] { .page-header.sticky { animation: slideOutUp_pageHeader $slide-up-common-duration forwards; } } @keyframes slideOutUp_leftColumnAndSearchBar { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(0, -100%, 0); } } .slideOutUp_leftColumnAndSearchBar { animation-name: slideOutUp_leftColumnAndSearchBar; } [data-scroll-direction="down"] { .page-leftColumn.sticky, .page-searchBar.sticky { animation: slideOutUp_leftColumnAndSearchBar $slide-up-common-duration forwards; } //.page-leftColumn.sticky { // animation: slideOutUp_leftColumnAndSearchBar 0.64s forwards; //} //.page-searchBar.sticky { // animation: slideOutUp_leftColumnAndSearchBar 1.0s forwards; //} } |
Une fonction javascript qui s’occupe d’appliquer le margin-top
(qui peut posséder une valeur dynamique):
|
function adjustSticky() { let $leftColumn = document.querySelector('.page-leftColumn'), computedStyle = getComputedStyle($leftColumn), leftColumnHeight = $leftColumn.clientHeight - (parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)), searchBarHeight = $('.page-searchBar').closest('.sticky-container').attr('style'), regex = /[^0-9.]/g; // console.log(leftColumnHeight+', '+searchBarHeight.replace(regex, '')); $('#adjustSticky').remove(); $('<style id="adjustSticky" type="text/css">@media screen and (min-width: 64em) { .page-leftColumn.sticky[style*="0em"]{ margin-top: '+leftColumnHeight+'px !important } .page-searchBar.sticky[style*="0em"]{ margin-top: '+searchBarHeight.replace(regex, '')+'px !important } }</style>').appendTo('body'); } |
…qu’on applique avec un tout petit délais histoire de laisser le temps au DOM de se mettre à jour:
Ancienne version:
Ma nouvelle animation:
|
// Custom animation for .page-leftColumn and .page-searchBar "smooth" sticky @keyframes slideOutUp_leftColumnAndSearchBar { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(0, -100%, 0); } } .slideOutUp_leftColumnAndSearchBar { animation-name: slideOutUp_leftColumnAndSearchBar; } |
…appliquée à mes éléments avec l’ajout de forwards
:
|
[data-scroll-direction="down"] { .page-leftColumn.sticky[style*="0em"], .page-searchBar.sticky[style*="0em"] { animation: slideOutUp_leftColumnAndSearchBar 0.3s forwards; } //Ajuster la durée des animation pour caler les éléments si ils ne sont pas de la même hauteur //.page-leftColumn.sticky[style*="0em"] { // animation: slideOutUp_leftColumnAndSearchBar 0.64s forwards; //} //.page-searchBar.sticky[style*="0em"] { // animation: slideOutUp_leftColumnAndSearchBar 1.0s forwards; //} } |
Une fonction javascript qui s’occupe d’appliquer le margin-top
(qui peut posséder une valeur dynamique):
|
function adjustSticky() { let leftColumnHeight = $('.page-leftColumn').closest('.sticky-container').attr('style'), searchBarHeight = $('.page-searchBar').closest('.sticky-container').attr('style'); // console.log(leftColumnHeight.replace(/\D+/g, '')+', '+searchBarHeight.replace(/\D+/g, '')); $('#adjustSticky').remove(); $('<style id="adjustSticky" type="text/css">@media screen and (min-width: 64em) { .page-leftColumn.sticky[style*="0em"]{ margin-top: '+leftColumnHeight.replace(/\D+/g, '')+'px !important } .page-searchBar.sticky[style*="0em"]{ margin-top: '+searchBarHeight.replace(/\D+/g, '')+'px !important } }</style>').appendTo('body'); } |
…qu’on applique avec un tout petit délais histoire de laisser le temps au DOM de se mettre à jour:
|
setTimeout(adjustSticky(), 50); |