Fermer

juillet 21, 2021

Construire un entête dynamique avec Intersection Observer —


Résumé rapide ↬

Avez-vous déjà eu besoin de créer une interface utilisateur où certains composants de la page doivent répondre aux éléments lorsqu'ils défilent jusqu'à un certain seuil dans la fenêtre – ou peut-être dans et hors de la fenêtre elle-même ? En JavaScript, attacher un écouteur d'événement pour déclencher en permanence un rappel lors du défilement peut être gourmand en performances et, s'il est utilisé de manière imprudente, peut rendre l'expérience utilisateur lente. Mais il existe un meilleur moyen avec Intersection Observer.

L'Intersection Observer API est une API JavaScript qui nous permet d'observer un élément et de détecter quand il passe un point spécifié dans un conteneur de défilement — souvent (mais pas toujours) la fenêtre d'affichage — déclenchant une fonction de rappel .

Intersection Observer peut être considéré comme plus performant que l'écoute des événements de défilement sur le thread principal, car il est asynchrone, et le rappel ne se déclenchera que lorsque l'élément que nous observons atteint le seuil spécifié, au lieu de chaque fois que la position de défilement Est mis à jour. Dans cet article, nous allons vous montrer comment utiliser Intersection Observer pour créer un composant d'en-tête fixe qui change lorsqu'il croise différentes sections de la page Web.

Utilisation de base

Pour utiliser Intersection Observer, nous besoin de créer d'abord un nouvel observateur, qui prend deux paramètres : un objet avec les options de l'observateur, et la fonction de rappel que nous voulons exécuter chaque fois que l'élément que nous observons (appelé la cible de l'observateur) croise la racine (le défilement conteneur, qui doit être un ancêtre de l'élément cible).

const options = {
  racine : document.querySelector('[data-scroll-root]'),
  rootMargin: '0px',
  seuil : 1.0
}

rappel const = (entrées, observateur) => {
  entrées.forEach((entrée) => console.log(entrée))
}

const observer = new IntersectionObserver (rappel, options)

Lorsque nous avons créé notre observateur, nous devons ensuite lui demander de surveiller un élément cible :

const targetEl = document.querySelector('[data-target]')

observer.observe(cibleEl)

Toutes les valeurs d'options peuvent être omises, car elles reviendront à leurs valeurs par défaut :

const options = {
  rootMargin: '0px',
  seuil : 1.0
}

Si aucune racine n'est spécifiée, elle sera classée comme la fenêtre d'affichage du navigateur. L'exemple de code ci-dessus montre les valeurs par défaut pour rootMargin et threshold. Celles-ci peuvent être difficiles à visualiser, elles valent donc la peine d'être expliquées :

rootMargin

La valeur rootMargin est un peu comme ajouter des marges CSS à l'élément racine – et, tout comme les marges, peut prendre plusieurs valeurs , y compris les valeurs négatives. L'élément cible sera considéré comme se coupant par rapport aux marges.

La racine de défilement avec des valeurs de marge de racine positives et négatives. Le carré orange est positionné à l'endroit où il serait classé comme « intersectant », en supposant une valeur de seuil par défaut de 1. ( Grand aperçu)

Cela signifie qu'un élément peut techniquement être classé comme « intersectant » même lorsqu'il est hors de vue (si notre racine de défilement est la fenêtre d'affichage).

Le carré orange est en intersection avec la racine, même s'il est à l'extérieur la zone visible. ( Grand aperçu)

rootMargin est défini par défaut sur 0pxmais peut accepter une chaîne composée de plusieurs valeurs, tout comme l'utilisation de la propriété margin dans CSS.

threshold[19659013]Le seuil peut être constitué d'une seule valeur ou d'un tableau de valeurs entre 0 et 1. Il représente la proportion de l'élément qui doit être dans les limites de la racine pour qu'il soit considéré comme une intersection. En utilisant la valeur par défaut de 1, le rappel se déclenche lorsque 100 % de l'élément cible est visible dans la racine.

Les seuils de 1, 0 et 0,5 entraînent respectivement le déclenchement du rappel lorsque 100 %, 0 % et 50 % est visible. ( Grand aperçu)

Il n'est pas toujours facile de visualiser quand un élément sera classé comme visible à l'aide de ces options. J'ai construit un petit outil pour vous aider à vous familiariser avec Intersection Observer.

Plus après le saut ! Continuez à lire ci-dessous ↓

Maintenant que nous avons compris les principes de base , commençons à créer notre en-tête dynamique. Nous allons commencer par une page Web divisée en sections. Cette image montre la mise en page complète de la page que nous allons créer :

( Grand aperçu)

J'ai inclus une démo à la fin de cet article, alors n'hésitez pas à y accéder directement si vous souhaitez désélectionner le code. (Il existe également un dépôt Github.)

Chaque section a une hauteur minimale de 100vh (bien qu'elle puisse être plus longue, selon le contenu). Notre en-tête est fixé en haut de la page et reste en place pendant que l'utilisateur fait défiler (en utilisant position: fixed). Les sections ont des arrière-plans de couleurs différentes et lorsqu'elles rencontrent l'en-tête, les couleurs de l'en-tête changent pour compléter celles de la section. Il y a aussi un marqueur pour montrer la section actuelle dans laquelle se trouve l'utilisateur, qui glisse lorsque la section suivante arrive.
Pour nous permettre d'accéder plus facilement au code pertinent, j'ai mis en place une démo minimale avec notre point de départ (avant de commencer à utiliser l'API Intersection Observer), au cas où vous voudriez suivez.

Markup

Nous allons commencer par le code HTML de notre en-tête. Ce sera un en-tête assez simple avec un lien d'accueil et une navigation, rien de particulièrement sophistiqué, mais nous allons utiliser quelques attributs de données : data-header pour l'en-tête lui-même (donc nous pouvons cibler l'élément avec JS), et trois liens d'ancrage avec l'attribut data-linkqui fera défiler l'utilisateur jusqu'à la section appropriée lorsqu'il sera cliqué :

Ensuite, le HTML pour le reste de notre page, qui est divisé en sections. Par souci de concision, je n'ai inclus que les parties pertinentes à l'article, mais le balisage complet est inclus dans la démo. Chaque section comprend un attribut de données spécifiant le nom de la couleur d'arrière-plan et un id qui correspond à l'un des liens d'ancrage dans l'en-tête :

Nous positionnerons notre en-tête avec CSS afin qu'il reste fixe en haut de la page pendant que l'utilisateur fait défiler :

header {
  position : fixe ;
  largeur : 100 % ;
}

Nous allons également donner à nos sections une hauteur minimale et centrer le contenu. (Ce code n'est pas nécessaire pour que l'Intersection Observer fonctionne, c'est juste pour la conception.)

section {
  rembourrage : 5rem 0 ;
  hauteur minimale : 100 vh ;
  affichage : flexible ;
  justifier-contenu : centre ;
  align-items: center;
}

Avertissement iframe

Lors de la création de cette démo Codepen, j'ai rencontré un problème déroutant où mon code Intersection Observer qui aurait dû fonctionnait parfaitement ne réussissait pas à déclencher le rappel au bon point de l'intersection mais au lieu de se déclencher lorsque l'élément cible a croisé le bord de la fenêtre. Après un peu de grattage, j'ai réalisé que c'était parce que dans Codepen, le contenu est chargé dans un iframe, qui est traité différemment. (Voir la section de la documentation MDN sur Découpage et rectangle d'intersection pour plus de détails.)

Comme solution de contournement, dans la démo, nous pouvons envelopper notre balisage dans un autre élément, qui servira de défilement conteneur - la racine dans nos options d'E/S - plutôt que la fenêtre d'affichage du navigateur, comme on pourrait s'y attendre :

Si vous voulez voir comment utiliser la fenêtre d'affichage comme racine à la place pour la même démo, cela est inclus dans le référentiel Github.

CSS

Dans notre CSS, nous définirons certaines propriétés personnalisées pour les couleurs que nous utilisons. Nous définirons également deux propriétés personnalisées supplémentaires pour le texte de l'en-tête et les couleurs d'arrière-plan, et définirons quelques valeurs initiales. (Nous allons mettre à jour ces deux propriétés personnalisées pour les différentes sections plus tard.)

:root {
  --mint : #5ae8d5 ;
  --chocolat: #573e31;
  --framboise : #f2308e ;
  --vanille: #faf2c8;
  
  --headerText: var(--vanilla);
  --headerBg: var(--framboise);
}

Nous utiliserons ces propriétés personnalisées dans notre en-tête :

header {
  couleur d'arrière-plan : var(--headerBg);
  couleur : var(--headerText);
}

Nous allons également définir les couleurs de nos différentes sections. J'utilise les attributs de données comme sélecteurs, mais vous pouvez tout aussi bien utiliser une classe si vous préférez.

[data-section="raspberry"] {
  background-color: var(--framboise);
  couleur: var(--vanille);
}

[data-section="mint"] {
  couleur d'arrière-plan : var(--mint);
  couleur: var(--chocolat);
}

[data-section="vanilla"] {
  couleur d'arrière-plan : var(--vanille);
  couleur: var(--chocolat);
}

[data-section="chocolate"] {
  background-color: var(--chocolat);
  couleur: var(--vanille);
}

Nous pouvons également définir des styles pour notre en-tête lorsque chaque section est visible :

/* En-tête */
[data-theme="raspberry"] {
  --headerText: var(--raspberry);
  --headerBg: var(--vanille);
}

[data-theme="mint"] {
  --headerText: var(--mint);
  --headerBg: var(--chocolat);
}

[data-theme="chocolate"] {
  --headerText: var(--chocolat);
  --headerBg: var(--vanille);
}

Il y a de plus fortes raisons d'utiliser des attributs de données ici, car nous allons basculer l'attribut data-theme de l'en-tête à chaque intersection.

Création de l'observateur

Maintenant que nous avons le HTML et CSS de base pour la configuration de notre page, nous pouvons créer un observateur pour surveiller chacune de nos sections qui s'affichent. Nous voulons déclencher un rappel chaque fois qu'une section entre en contact avec le bas de l'en-tête lorsque nous faisons défiler la page. Cela signifie que nous devons définir une marge racine négative qui correspond à la hauteur de l'en-tête.

const header = document.querySelector('[data-header]')
sections const = [...document.querySelectorAll('[data-section]')]
const scrollRoot = document.querySelector('[data-scroller]')

options constantes = {
  racine : scrollRoot,
  rootMargin : `${header.offsetHeight * -1}px`,
  seuil : 0
}

Nous définissons un seuil de 0car nous voulons qu'il se déclenche si une partie de la section croise la marge racine.

Tout d'abord, nous allons créer un rappel pour modifier la valeur data-theme de l'en-tête. (C'est plus simple que d'ajouter et de supprimer des classes, en particulier lorsque notre élément d'en-tête peut avoir d'autres classes appliquées.)

/* Le rappel qui se déclenchera à l'intersection */
const onIntersect = (entrées) => {
  entrées.forEach((entrée) => {
    const theme = entry.target.dataset.section
    header.setAttribute('data-theme', theme)
  })
}

Ensuite, nous allons créer l'observateur pour surveiller les sections qui se croisent :

/* Créer l'observateur */
const observer = new IntersectionObserver(onIntersect, options)

/* Configurer notre observateur pour observer chaque section */
sections.forEach((section) => {
  observateur.observe(section)
})

Maintenant, nous devrions voir nos couleurs d'en-tête se mettre à jour lorsque chaque section rencontre l'en-tête.

Voir le stylo [Happy Face Ice Cream Parlour – Step 2](https://codepen.io/smashingmag/pen/poPgpjZ) par Michelle Barker.

Voir le stylo Happy Face Ice Cream Parlor - Step 2 par Michelle Barker.

Cependant, vous remarquerez peut-être que les couleurs ne se mettent pas à jour correctement car nous défiler vers le bas. En fait, l'en-tête se met à jour avec les couleurs de la section précédente à chaque fois ! En défilant vers le haut, en revanche, cela fonctionne parfaitement. Nous devons déterminer la direction de défilement et modifier le comportement en conséquence.

Trouver la direction de défilement

Nous allons définir une variable dans notre JS pour la direction de défilement, avec une valeur initiale de 'up'et une autre pour la dernière position de défilement connue (prevYPosition). Ensuite, dans le rappel, si la position de défilement est supérieure à la valeur précédente, nous pouvons définir la valeur direction comme 'down'ou 'up' si vice versa.

let direction = 'up'
laisser prevYPosition = 0

const setScrollDirection = () => {
  if (scrollRoot.scrollTop > prevYPosition) {
    direction = 'vers le bas'
  } autre {
    direction = 'haut'
  }

  prevYPosition = scrollRoot.scrollTop
}

const onIntersect = (entrées, observateur) => {
  entrées.forEach((entrée) => {
    setScrollDirection()
          
    /* ... */
  })
}

Nous allons également créer une nouvelle fonction pour mettre à jour les couleurs de l'en-tête, en passant la section cible en argument :

const updateColors = (target) => {
  const theme = target.dataset.section
  header.setAttribute('data-theme', theme)
}

const onIntersect = (entrées) => {
  entrées.forEach((entrée) => {
    setScrollDirection()
    updateColors(entry.target)
  })
}

Jusqu'à présent, nous ne devrions voir aucun changement dans le comportement de notre en-tête. Mais maintenant que nous connaissons le sens de défilement, nous pouvons passer une cible différente pour notre fonction updateColors(). Si le sens de défilement est vers le haut, nous utiliserons la cible d'entrée. S'il est en panne, nous utiliserons la section suivante (s'il y en a une).

const getTargetSection = (target) => {
  if (direction === 'up') renvoie la cible
  
  if (cible.nextElementSibling) {
    return target.nextElementSibling
  } autre {
    objectif de retour
  }
}

const onIntersect = (entrées) => {
  entrées.forEach((entrée) => {
    setScrollDirection()
    
    const cible = getTargetSection(entry.target)
    updateCouleurs(cible)
  })
}

Il y a cependant un autre problème : l'en-tête se met à jour non seulement lorsque la section atteint l'en-tête, mais lorsque l'élément suivant apparaît en bas de la fenêtre. C'est parce que notre observateur déclenche le rappel deux fois : une fois lorsque l'élément entre et une autre lorsqu'il sort. ]entrée objet. Créons une autre fonction pour renvoyer une valeur booléenne indiquant si les couleurs de l'en-tête doivent être mises à jour :

const shouldUpdate = (entry) => {
  if (direction === 'bas' && !entry.isIntersecting) {
    retourner vrai
  }
  
  if (direction === 'up' && entry.isIntersecting) {
    retourner vrai
  }
  
  retourner faux
}

Nous mettrons à jour notre fonction onIntersect() en conséquence :

const onIntersect = (entries) => {
  entrées.forEach((entrée) => {
    setScrollDirection()
    
    /* Ne rien faire si aucune mise à jour n'est nécessaire */
    if (!shouldUpdate(entry)) return
    
    const cible = getTargetSection(entry.target)
    updateCouleurs(cible)
  })
}

Maintenant, nos couleurs devraient se mettre à jour correctement. Nous pouvons définir une transition CSS, afin que l'effet soit un peu plus agréable :

header {
  transition : couleur de fond 200 ms, couleur 200 ms ;
}

Voir le stylo [Happy Face Ice Cream Parlour – Step 3](https://codepen.io/smashingmag/pen/bGWEaEa) par Michelle Barker.

Voir le stylo Happy Face Ice Cream Parlor – Step 3 par Michelle Barker.

Ajout du marqueur dynamique

Ensuite, nous ajouterons un marqueur à l'en-tête qui met à jour sa position lorsque nous faisons défiler les différentes sections. Nous pouvons utiliser un pseudo-élément pour cela, nous n'avons donc pas besoin d'ajouter quoi que ce soit à notre code HTML. Nous allons lui donner un style CSS simple pour le positionner en haut à gauche de l'en-tête et lui donner une couleur d'arrière-plan. Nous utilisons currentColor pour cela, car il prendra la valeur de la couleur du texte de l'en-tête :

header::after {
  contenu: '';
  position : absolue ;
  haut : 0 ;
  à gauche : 0 ;
  hauteur : 0.4rem ;
  background-color: currentColor;
}

Nous pouvons utiliser une propriété personnalisée pour la largeur, avec une valeur par défaut de 0. Nous utiliserons également une propriété personnalisée pour la valeur translate x. Nous allons définir les valeurs pour ceux-ci dans notre fonction de rappel au fur et à mesure que l'utilisateur fait défiler.

header::after {
  contenu: '';
  position : absolue ;
  haut : 0 ;
  à gauche : 0 ;
  hauteur : 0.4rem ;
  largeur : var(--markerWidth, 0);
  background-color: currentColor;
  transform: translate3d(var(--markerLeft, 0), 0, 0);
}

Nous pouvons maintenant écrire une fonction qui mettra à jour la largeur et la position du marqueur au point d'intersection :

const updateMarker = (target) => {
  id const = cible.id
  
  /* Ne rien faire si pas d'identifiant cible */
  si (!id) retourne
  
  /* Trouvez le lien de navigation correspondant, ou utilisez le premier */
  let link = headerLinks.find((el) => {
    return el.getAttribute('href') === `#${id}`
  })
  
  lien = lien || en-têteLiens[0]
  
  /* Récupère les valeurs et définit les propriétés personnalisées */
  const distanceFromLeft = link.getBoundingClientRect().left
  
  header.style.setProperty('--markerWidth', `${link.clientWidth}px`)
  header.style.setProperty('--markerLeft', `${distanceFromLeft}px`)
}

Nous pouvons appeler la fonction en même temps que nous mettons à jour les couleurs :

const onIntersect = (entries) => {
  entrées.forEach((entrée) => {
    setScrollDirection()
    
    if (!shouldUpdate(entry)) return
    
    const cible = getTargetSection(entry.target)
    updateCouleurs(cible)
    updateMarker(cible)
  })
}

Nous devrons également définir une position initiale pour le marqueur, afin qu'il n'apparaisse pas de nulle part. Lorsque le document est chargé, nous appellerons la fonction updateMarker()en utilisant la première section comme cible :

document.addEventListener('readystatechange', e => {
  if (e.target.readyState === 'complete') {
    updateMarker(sections[0])
  }
})

Enfin, ajoutons une transition CSS pour que le marqueur glisse sur l'en-tête d'un lien à l'autre. Alors que nous effectuons la transition de la propriété widthnous pouvons utiliser will-change pour permettre au navigateur d'effectuer des optimisations.

header::after {
  transition : transformer 250 ms, largeur 200 ms, couleur d'arrière-plan 200 ms ;
  va-changer : largeur ;
}

Pour une touche finale, ce serait bien si, lorsqu'un utilisateur clique sur un lien, il fait défiler la page en douceur, au lieu de sauter à la section. De nos jours, nous pouvons le faire directement dans notre CSS, aucun JS requis ! Pour une expérience plus accessible, il est judicieux de respecter les préférences de mouvement de l'utilisateur en implémentant un défilement fluide uniquement s'il n'a pas spécifié de préférence pour un mouvement réduit dans ses paramètres système :

@media (prefers-reduced-motion : non -préférence) {
  .scroller {
    comportement de défilement : lisse ;
  }
}

Démo finale

Rassembler toutes les étapes ci-dessus permet d'obtenir la démo complète.

Voir le stylo [Happy Face Ice Cream Parlour – Intersection Observer example](https://codepen.io/smashingmag/pen/XWRXVXQ) de Michelle Barker .

Voir le Pen Happy Face Ice Cream Parlor – Intersection Observer example par Michelle Barker.

Browser Support

Intersection Observer est largement pris en charge dans les navigateurs modernes. Si nécessaire, il peut être polyfilled pour les navigateurs plus anciens – mais je préfère adopter une approche d'amélioration progressive lorsque cela est possible. Dans le cas de notre en-tête, il ne serait pas très préjudiciable à l'expérience utilisateur de fournir une version simple et immuable pour les navigateurs non compatibles.

Pour détecter si Intersection Observer est pris en charge, nous pouvons utiliser ce qui suit :

 if ('IntersectionObserver' dans la fenêtre && 'IntersectionObserverEntry' dans la fenêtre && 'intersectionRatio' dans window.IntersectionObserverEntry.prototype) {
  /* Code à exécuter si IO est supporté */
} autre {
  /* Code à exécuter si non supporté */
}

Ressources

En savoir plus sur Intersection Observer :

  • Documentation complète, avec quelques exemples pratiques de MDN
  • Intersection Observer outil de visualisation
  • Timing Element Visibility with the Intersection API Observer – un autre didacticiel de MDN, qui explique comment l'IO peut être utilisé pour suivre la visibilité des annonces
  • Cet article de Denys Mishunov couvre d'autres utilisations de l'IO, y compris les ressources de chargement paresseux. Bien que cela soit moins nécessaire maintenant (grâce à l'attribut loading), il y a encore beaucoup à apprendre ici.
Smashing Editorial(vf, il)




Source link