Introduction
Je me souviendrai toujours du beau matin où j'ai appris l'existence de l'IntersectionObserver, une API de navigateur native permettant que diverses chose se produisent lorsque les utilisateurs font défiler la page ou n'importe quel élément avec des barres de défilement.
Enfin quelque chose d'économe en événements qui soit presque carrément élégant pour implémenter tout ce qu'il y a de plus cool dans la vie:
- Chargement différé (lazy loading) de contenu (par ex. des images);
- Un peu la même idée: chargement de plus de contenu quand on arrive à la fin de la page (infinite scrolling) — C'est ce qui fait qu'on reste des heures à regarder des vidéos de bouffe sur Facebook ou des reels tout naze sur Instagram mais on peut pas s'en empêcher même si on sait pertinemment bien que c'est la même chose que rediriger sa vie dans /dev/null ;
- Montrer d'autant plus impressionnantes qu'inutiles animations pour révéler du contenu ou montrer la progression.
J'avais déjà sommairement exploré le point 3 en l'implémentant sur les images des articles pour la future mouture (ça fait au moins 10 ans que je bosse dessus) du présent blog, ça donnait ceci (**il faut défiler la page**):
Je veux animer du TEXTE
Pour la petite histoire, j'ai voulu tenter d'animer des trucs de texte en voyant la page d'accueil de l'agence web "Epic Agency".
Leur site est bourré d'animations en tout genre, de déformations et d'objets générés dans des éléments canvas.
Bon alors c'est sûr que leur site illustre assez bien si vous êtes privilégiés ou pas puisqu'il va être lent et consommer toutes les ressources sur votre ordi/téléphone moisi de 2010 que vous trainez toujours.

Ne soyons pas rabatteurs de joie, voici l'animation dont qu'est-ce que laquelle je parle:
Je dois dire que mes résultats sont... Mitigés. Je vais pas pouvoir tout de suite postuler dans cette agence web. Ou alors je dois tout miser sur mes talents de dessinateur vectoriel.
Et là je varie juste l'opacité, on est loins des incroyables transformations géométriques qui m'intéressaient initialement.
Je me suis perdu dans les méandres des tentatives de détecter si la personne défile vers le haut ou le bas et essayer de re-transitionner les mots et puis un moment j'ai accepté ma défaite.
Le problème
Il y a plusieurs problèmes. J'ai beaucoup de problèmes.
Si je veux que mon texte soit initialement déplacé hors de vue avec un transform et une rotation et/ou translation, ça ne déclenche pas l'IntersectionObserver, parce que l'objet n'est pas en vue. Logique somme toute.
J'avais pensé à un pseudo-element de type ::after ou ::before pour ajouter un morceau à mon titre, genre comme ceci:
.titre-nul {
transform-origin: top left;
display: flex;
justify-content: space-between;
}
.titre-nul::after {
content: 'coucou';
background-color: deeppink;
color: deeppink;
}
Oui c'est vraiment nul comme plan, j'ai un élément qui pend à droite de mon titre qui aide à "le rendre visible" à l'IntersectionObserver:
C'est presque fontionnel, comme beaucoup de choses dans ma vie. C'est aussi évident que je devrais trouver un autre hobby.
Plan ultime (lol): utiliser un élément de substitution
Ce serait plus utile que l'IntersectionObserver surveille un élément qui n'est pas le truc que je veux animer mais un genre de spéarateur bien droit qui prend bien toute la largeur de la page et va bien intersecter comme il faut.
On est pas obligés que cet élément soit visible, une opacité de 0 ou une visbility sur hidden ça déclenche quand même l'IntersectionObserver. Alors que c'est pas visible. Oui bah c'est comme ça hein.
Par contre, avec l'attribut hidden, ça marche pas. Et évidemment pas non plus avec display: none mais ça, ça me parait logique.
Je vais utiliser JavaScript pour ajouter dynamiquement ces éléments de substitutions, et les placer juste avant chaque titre concerné par l'animation de rotation/translation.
J'ai décidé de juste appliquer cette animation sur les tags h1 et h2, sur les autres je fais une translation vers le bas donc le titre est toujours en vue.
Mon élément de substitution sera un hr.
Les styles qui vont bien:
/* Titles reveal animation for articles */
h1.pre-animate,
h2.pre-animate {
transform-origin: top left;
transform: rotate(-100deg) translateY(-500%);
opacity: 0;
}
h3.pre-animate,
h4.pre-animate,
h5.pre-animate {
transform: translateY(-200%);
opacity: 0;
}
.pre-animate-transition {
transition: transform 0.5s ease-out, opacity 0.8s ease-out;
}
.animation-placeholder {
/* Still works with intersection observer */
visibility: hidden;
position: absolute;
height: 16em;
width: 80%;
}
Puisqu'on a besoin de JavaScript pour opérer la transition, je l'utilise pour activer l'état initial des titres aussi. C'est-à-dire que les titres apparaîtront normalement pour quelqu'un qui n'a pas JavaScript activé.
Commençons par déclarer quelques trucs:
// Intersection observer to reveal titles:
const articleContent = document.querySelector(".article-content")
const titles = Array.from(articleContent.querySelectorAll(
"h1, h2, h3, h4"
))
// Has to be uppercase for browser API reasons of the past:
const placeholderTag = "HR"
const requiresPlaceholder = (node) =>
node.tagName === "H1" || node.tagName === "H2"
On déclare l'observer:
const titleObserver = new IntersectionObserver((entries, observer) => {
for (const en of entries) {
if (en.isIntersecting === true) {
const elementRef = en.target.tagName === placeholderTag ?
en.target.nextSibling : en.target
if (!elementRef.getAttribute("data-animated")) {
elementRef.classList.add("pre-animate-transition")
elementRef.setAttribute("data-animated", true)
// WE CAN UNOBSERVE A TRANSFORMED ITEM
observer.unobserve(en.target)
elementRef.style.opacity = 1
elementRef.style.transform = `rotate(${0}deg) translate(0%, 0%)`
}
}
}
},
{
threshold: 0.25,
root: document
})
Et finalement, on l'applique aux bons titres et on ajoute la classe de l'état initial de l'animation:
for (const t of titles) {
t.classList.add("pre-animate")
if (requiresPlaceholder(t)) {
// Special animation with placeholders for h1 and h2:
const ph = document.createElement(placeholderTag)
// Hidden doesn't work with intersection obs but
// CSS visibility hidden does. OKAY
//ph.hidden = true
ph.classList.add("animation-placeholder")
articleContent.insertBefore(ph, t)
titleObserver.observe(ph)
} else {
titleObserver.observe(t)
}
}
Ce qui donne ce résultat — un premier jet n'est-ce-pas (que je vais sûrement conserver inchangé pour toujours):
Il y a juste un petit soucis avec Firefox (ben tiens) quand on clique sur un lien type anchor pour se rendre directement sous un certain titre (par ex. avec la table des matières): le titre est immédiatement en vue mais ça ne déclenche pas l'IntersectionObserver.
Ce soucis n'existe pas sous Chrome et toute sa famille incestueuse qui forme la majorité des navigateurs utilisés.
Je comprends un peu la logique (si je veux être très généreux avec ce pauvre Firefox qui est tout seul et tout nu dans les bois depuis un moment), on a pas vraiment défilé jusqu'au titre, la page s'est posée là d'elle-même. Mais moi ça m'arrange pas.
Ajouter "smooth-scrolling" n'aide pas non plus.
Voyez ce Codepen pour une illustration (ça fonctionne sous Chrome, hein, pas la peine de tester avec Chrome :D).
Si quelqu'un a une autre manière de procéder qui ne consiste pas à ajouter des éléments inutiles invisibles, je suis preneur et vous prie de laisser votre ample trace dans les commentaires ci-dessous.

Commentaires
Il faut JavaScript activé pour écrire des commentaires ici