TL;DR
Voici un secret: vous n'aurez peut-être pas besoin d'événements scroll
dans votre prochaine application. À l'aide d'un
IntersectionObserver
,
Je vous montre comment déclencher un événement personnalisé lorsque des éléments position:sticky
deviennent fixes ou qu'ils cessent de persister. Le tout sans utiliser
l'utilisation d'écouteurs de défilement. Une démonstration géniale peut même le prouver:
Découvrez l'événement sticky-change
L'une des limites pratiques de l'utilisation de la position persistante CSS est qu'elle ne fournit pas de signal de plate-forme permettant de savoir quand la propriété est active. En d'autres termes, il n'existe aucun événement permettant de savoir quand un élément devient figé ou elle n'est plus collante.
Prenons l'exemple suivant, qui corrige une <div class="sticky">
de 10 px par rapport au
en haut de son conteneur parent:
.sticky {
position: sticky;
top: 10px;
}
Ne serait-il pas agréable que le navigateur vous informe lorsque les éléments atteignent cette marque ?
Apparemment, je ne suis pas le seul
qui pense que oui. Un signal pour position:sticky
peut débloquer plusieurs cas d'utilisation:
- Appliquez une ombre projetée à une bannière au fur et à mesure qu'elle adhère.
- Lorsqu'un utilisateur lit votre contenu, enregistrez les appels Analytics pour connaître son la progression.
- Lorsque l'utilisateur fait défiler la page, mettre à jour un widget de table des matières flottant avec la version actuelle .
En tenant compte de ces cas d'utilisation, nous nous sommes fixé un objectif final: créer un événement
se déclenche lorsqu'un élément position:sticky
est fixe. Appelons-la
Événement sticky-change
:
document.addEventListener('sticky-change', e => {
const header = e.detail.target; // header became sticky or stopped sticking.
const sticking = e.detail.stuck; // true when header is sticky.
header.classList.toggle('shadow', sticking); // add drop shadow when sticking.
document.querySelector('.who-is-sticking').textContent = header.textContent;
});
La démonstration utilise cet événement aux en-têtes d'une ombre projetée lorsqu'ils sont corrigés. Elle met également à jour nouveau titre en haut de la page.
<ph type="x-smartling-placeholder">Effets de défilement sans événements de défilement ?
<ph type="x-smartling-placeholder">![Structure de la page.](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/structure-the-page-de19d5c111bd1.png?authuser=19&hl=fr)
Définissons quelques termes pour que je puisse m’y référer dans le reste de l’article:
- Conteneur de défilement : zone de contenu (fenêtre d'affichage visible) contenant liste des "articles de blog".
- En-têtes : titre en bleu dans chaque section contenant
position:sticky
. - Sections persistantes : chaque section de contenu. Texte qui défile sous en-têtes persistants.
- Mode persistant : lorsque
position:sticky
est appliqué à l'élément.
Pour savoir quel en-tête passe en "mode persistant", nous avons besoin d'un moyen de déterminer
Décalage de défilement du conteneur de défilement. Cela nous donnerait un moyen
pour calculer l'en-tête qui s'affiche. Cependant, cela devient assez
difficile à faire sans les événements scroll
. L'autre problème est que
position:sticky
supprime l'élément de la mise en page lorsqu'il devient fixe.
Sans les événements de défilement, nous avons perdu la possibilité d'effectuer des calculs sur les en-têtes.
Ajout d'un DOM factice pour déterminer la position de défilement
Au lieu d'événements scroll
, nous allons utiliser un IntersectionObserver
pour
déterminer quand les en-têtes entrent et quittent le mode rémanent. Ajouter deux nœuds
(aussi appelées sentinelles) dans chaque section persistante, une en haut et une
en bas de l'écran serviront de points de cheminement pour déterminer la position de défilement. Comme ces
les repères entrent dans le conteneur ou le quittent, leur visibilité change et
Intersection Observer déclenche un rappel.
![Absence d'éléments sentinelles](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/without-sentinel-elements-f9529d3483c91.gif?authuser=19&hl=fr)
Nous avons besoin de deux sentinelles pour couvrir quatre cas de défilement vers le haut et vers le bas:
- Défilement vers le bas : l'en-tête devient persistant lorsque sa sentinelle supérieure croise en haut du conteneur.
- Défilement vers le bas : header quitte le mode persistant lorsqu'il atteint le bas de la section et sa sentinelle inférieure traverse le haut du conteneur.
- Faire défiler vers le haut : l'en-tête quitte le mode persistant lorsque sa sentinelle supérieure fait défiler la page. à nouveau dans la vue depuis le haut.
- Défilement vers le haut : l'en-tête devient persistant lorsque sa sentinelle inférieure se croise. dans la vue depuis le haut.
Il est utile de voir un enregistrement d'écran de 1 à 4 dans l'ordre dans lequel ils apparaissent:
<ph type="x-smartling-placeholder">Le CSS
Les sentinelles sont positionnées en haut et en bas de chaque section.
.sticky_sentinel--top
se trouve en haut de l'en-tête,
.sticky_sentinel--bottom
se trouve au bas de la section:
![La sentinelle inférieure atteint son seuil.](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/bottom-sentinel-reaching-208a13f07bd7c.png?authuser=19&hl=fr)
:root {
--default-padding: 16px;
--header-height: 80px;
}
.sticky {
position: sticky;
top: 10px; /* adjust sentinel height/positioning based on this position. */
height: var(--header-height);
padding: 0 var(--default-padding);
}
.sticky_sentinel {
position: absolute;
left: 0;
right: 0; /* needs dimensions */
visibility: hidden;
}
.sticky_sentinel--top {
/* Adjust the height and top values based on your on your sticky top position.
e.g. make the height bigger and adjust the top so observeHeaders()'s
IntersectionObserver fires as soon as the bottom of the sentinel crosses the
top of the intersection container. */
height: 40px;
top: -24px;
}
.sticky_sentinel--bottom {
/* Height should match the top of the header when it's at the bottom of the
intersection container. */
height: calc(var(--header-height) + var(--default-padding));
bottom: 0;
}
Configurer les observateurs d'intersection
Les observateurs d'intersection observent de manière asynchrone les modifications de l'intersection de un élément cible et la fenêtre d'affichage du document ou un conteneur parent. Dans notre cas, nous observons des intersections avec un conteneur parent.
La recette magique est IntersectionObserver
. Chaque sentinelle reçoit une
IntersectionObserver
pour observer la visibilité de l'intersection dans
conteneur de défilement. Lorsqu'une sentinelle défile dans la fenêtre d'affichage visible, nous savons
un en-tête est fixe ou n'est plus persistant. De même, lorsqu'une sentinelle quitte
la fenêtre d'affichage.
Tout d'abord, j'ai configuré des observateurs pour les sentinelles d'en-tête et de pied de page:
/**
* Notifies when elements w/ the `sticky` class begin to stick or stop sticking.
* Note: the elements should be children of `container`.
* @param {!Element} container
*/
function observeStickyHeaderChanges(container) {
observeHeaders(container);
observeFooters(container);
}
observeStickyHeaderChanges(document.querySelector('#scroll-container'));
Ensuite, j'ai ajouté un observateur à déclencher lorsque des éléments .sticky_sentinel--top
sont transmis
en haut du conteneur de défilement (dans les deux sens).
La fonction observeHeaders
crée les principales sentinelles et les ajoute à
chaque section. L'observateur calcule l'intersection de la sentinelle avec
en haut du conteneur et détermine s'il entre dans la fenêtre d'affichage ou en sort. Cela
les informations déterminent si l'en-tête de section est conservé ou non.
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--top` become visible/invisible at the top of the container.
* @param {!Element} container
*/
function observeHeaders(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
// Started sticking.
if (targetInfo.bottom < rootBoundsInfo.top) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.bottom >= rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [0], root: container});
// Add the top sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--top');
sentinels.forEach(el => observer.observe(el));
}
L'observateur est configuré avec threshold: [0]
, de sorte que son rappel se déclenche dès que
à mesure que la sentinelle devient visible.
Le processus est similaire pour la sentinelle inférieure (.sticky_sentinel--bottom
).
Un second observateur est créé pour se déclencher lorsque les pieds de page passent par le bas
du conteneur de défilement. La fonction observeFooters
crée la
les nœuds sentinelles et les associe à chaque section. L'observateur calcule
l'intersection de la sentinelle avec le bas du conteneur et détermine
à l'entrée ou à la sortie. Ces informations déterminent si l'en-tête de section
ou non.
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--bottom` become visible/invisible at the bottom of the
* container.
* @param {!Element} container
*/
function observeFooters(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
const ratio = record.intersectionRatio;
// Started sticking.
if (targetInfo.bottom > rootBoundsInfo.top && ratio === 1) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.top < rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [1], root: container});
// Add the bottom sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--bottom');
sentinels.forEach(el => observer.observe(el));
}
L'observateur est configuré avec threshold: [1]
. Son rappel est donc déclenché lorsque le
l'ensemble du nœud est visible.
Pour finir, j'ai les deux utilitaires permettant de déclencher l'événement personnalisé sticky-change
.
et générer les sentinelles:
/**
* @param {!Element} container
* @param {string} className
*/
function addSentinels(container, className) {
return Array.from(container.querySelectorAll('.sticky')).map(el => {
const sentinel = document.createElement('div');
sentinel.classList.add('sticky_sentinel', className);
return el.parentElement.appendChild(sentinel);
});
}
/**
* Dispatches the `sticky-event` custom event on the target element.
* @param {boolean} stuck True if `target` is sticky.
* @param {!Element} target Element to fire the event on.
*/
function fireEvent(stuck, target) {
const e = new CustomEvent('sticky-change', {detail: {stuck, target}});
document.dispatchEvent(e);
}
Et voilà !
Démonstration finale
Nous avons créé un événement personnalisé lorsque les éléments avec position:sticky
deviennent
Correction et ajout d'effets de défilement sans utiliser d'événements scroll
.
Conclusion
Je me suis souvent demandé si IntersectionObserver
le ferait
Il peut être utile de remplacer certains des modèles d'UI basés sur des événements scroll
qui
ont évolué au fil des années. Il s'avère que la réponse est oui et non. La sémantique
de l'API IntersectionObserver
rendent son utilisation difficile. Mais comme
que j’ai montré ici, vous pouvez l’utiliser
pour quelques techniques intéressantes.
Une autre façon de détecter les changements de style ?
Pas vraiment. Nous avions besoin d'un moyen d'observer les changements de style sur un élément DOM. Malheureusement, les API de la plate-forme Web ne vous permettent pas le style de visionnage.
MutationObserver
serait un premier choix logique, mais cela ne fonctionne pas pour
dans la plupart des cas. Par exemple, dans la démonstration, nous recevrons un rappel lorsque sticky
est ajoutée à un élément, mais pas lorsque son style calculé change.
Rappelez-vous que la classe sticky
a déjà été déclarée lors du chargement de la page.
À l'avenir,
"Observateur de mutation de style"
vers Mutation Observers peut être utile pour observer les changements
les styles calculés de l'élément.
position: sticky
.