TL;DR
Ecco un segreto: potresti non aver bisogno di eventi scroll
nella tua prossima app. Con un
IntersectionObserver
,
ti mostro come attivare un evento personalizzato quando gli elementi position:sticky
vengono corretti o quando non vengono più applicati. Il tutto senza ricorrere
ai listener di scorrimento. C'è anche una fantastica demo che lo dimostra:
Presentazione dell'evento sticky-change
Una delle limitazioni pratiche dell'utilizzo della posizione persistente CSS è che non fornisce un indicatore della piattaforma per sapere quando la proprietà è attiva. In altre parole, non c'è alcun evento in cui sia possibile sapere quando un elemento diventa fisso o smette di essere permanente.
Nell'esempio seguente, correggi un <div class="sticky">
di 10 px dalla parte superiore del contenitore principale:
.sticky {
position: sticky;
top: 10px;
}
Non sarebbe bello se il browser comunicasse quando gli elementi colpiscono quel segno?
A quanto pare non sono l'unico
a pensarlo. Un indicatore per position:sticky
potrebbe sbloccare una serie di casi d'uso:
- Applica un'ombra a un banner mentre si fissa.
- Mentre un utente legge i tuoi contenuti, registra gli hit di Analytics per conoscerne l'avanzamento.
- Mentre un utente scorre la pagina, aggiorna un widget Sommario mobile alla sezione corrente.
Tenendo a mente questi casi d'uso, abbiamo creato un obiettivo finale: creare un evento
che si attiva quando un elemento position:sticky
viene corretto. Chiamiamolo
evento 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 demo utilizza questo evento per intestazioni di ombra quando vengono corrette. Inoltre, aggiorna il nuovo titolo nella parte superiore della pagina.
Vuoi scorrere gli effetti senza eventi di scorrimento?
Togliamo un po' di terminologia in modo da poter fare riferimento a questi nomi nel resto del post:
- Contenitore a scorrimento: l'area dei contenuti (area visibile) contenente l'elenco dei "post del blog".
- Intestazioni: titolo blu in ogni sezione che contiene
position:sticky
. - Sezioni persistenti: ogni sezione di contenuti. Il testo che scorre sotto le intestazioni fisse.
- "Modalità persistente": quando all'elemento viene applicato
position:sticky
.
Per sapere quale intestazione entra in "modalità persistente", abbiamo bisogno di un modo per determinare
l'offset di scorrimento del container di scorrimento. Questo ci darebbe modo di calcolare l'intestazione attualmente visualizzata. Tuttavia, l'operazione può essere
complicata senza gli eventi scroll
. L'altro problema è che
position:sticky
rimuove l'elemento dal layout quando viene risolto.
Pertanto, senza eventi di scorrimento, abbiamo perso la possibilità di eseguire calcoli relativi al layout nelle intestazioni.
Aggiunta di un DOM dumby per determinare la posizione di scorrimento
Invece degli eventi scroll
, utilizzeremo un IntersectionObserver
per
determinare quando le headers entrano ed escono dalla modalità persistente. L'aggiunta di due nodi
(noti anche come sentinelle) in ogni sezione permanente, uno in alto e uno in basso, agirà da tappa per capire la posizione di scorrimento. Quando questi indicatori entrano ed escono dal container, la loro visibilità cambia e Intersection Observer attiva un callback.
Abbiamo bisogno di due sentinelle per coprire quattro casi di scorrimento verso l'alto e verso il basso:
- Scorrimento verso il basso: l'intestazione diventa fissa quando la sua sentinella superiore attraversa la parte superiore del container.
- Scorrere verso il basso: intestazione lascia la modalità persistente quando raggiunge la parte inferiore della sezione e la sentinella inferiore attraversa la parte superiore del container.
- Scorrimento verso l'alto: intestazione lascia la modalità persistente quando la parte superiore della sentinella torna visibile dall'alto.
- Scorrere verso l'alto: l'intestazione diventa fissa quando la sentinella inferiore torna alla vista dall'alto.
È utile vedere uno screencast di 1-4 nell'ordine in cui avvengono:
Il CSS
Le sentinelle sono posizionate nella parte superiore e inferiore di ogni sezione.
.sticky_sentinel--top
si trova nella parte superiore dell'intestazione, mentre
.sticky_sentinel--bottom
si trova in fondo alla sezione:
: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;
}
Configurare gli osservatori delle intersezioni
Gli osservatori delle intersezioni osservano in modo asincrono i cambiamenti nell'intersezione di un elemento target e nell'area visibile del documento o di un contenitore principale. Nel nostro caso, osserviamo le intersezioni con un contenitore principale.
La salsa magica è IntersectionObserver
. Ogni sentinella riceve un IntersectionObserver
per osservare la sua visibilità intersezione all'interno del container di scorrimento. Quando una sentinella scorre nell'area visibile, sappiamo che
un'intestazione diventa fissa o non è più fissa. Allo stesso modo, quando una sentinella
esce dall'area visibile.
Per prima cosa, ho impostato degli osservatori per le sentinelle di intestazione e piè di pagina:
/**
* 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'));
Quindi, ho aggiunto un osservatore per attivarsi quando gli elementi .sticky_sentinel--top
passano
nella parte superiore del container di scorrimento (in entrambe le direzioni).
La funzione observeHeaders
crea le sentinelle superiori e le aggiunge a
ogni sezione. L'osservatore calcola l'intersezione della sentinella
con la parte superiore del container e decide se entra o esce dall'area visibile. Queste informazioni determinano se l'intestazione della sezione viene applicata o meno.
/**
* 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'osservatore è configurato con threshold: [0]
, quindi il suo callback si attiva non appena la sentinella diventa visibile.
Il processo è simile per la sentinella inferiore (.sticky_sentinel--bottom
).
Viene creato un secondo osservatore che si attiva quando i piè di pagina passano attraverso la parte inferiore
del container a scorrimento. La funzione observeFooters
crea i nodi sentinel e li collega a ogni sezione. L'osservatore calcola l'intersezione della sentinella con il fondo del container e decide se entra o esce. Queste informazioni determinano
se l'intestazione della sezione viene applicata o meno.
/**
* 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'osservatore è configurato con threshold: [1]
, quindi il suo callback viene attivato quando
l'intero nodo è nella visualizzazione.
Infine, ci sono le due utilità per attivare l'evento personalizzato sticky-change
e generare le sentinelle:
/**
* @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);
}
È tutto.
Demo finale
Abbiamo creato un evento personalizzato quando gli elementi con position:sticky
sono stati corretti e abbiamo aggiunto effetti di scorrimento senza l'utilizzo di eventi scroll
.
Conclusione
Mi chiedevo spesso se IntersectionObserver
sarebbe stato uno strumento utile per sostituire alcuni dei pattern di UI basati su eventi di scroll
che si sono sviluppati nel corso degli anni. La risposta è sì e no. La semantica
dell'API IntersectionObserver
rende difficile da usare per tutto. Ma, come ho mostrato qui,
puoi usarlo per alcune tecniche interessanti.
Un altro modo per rilevare i cambiamenti di stile?
In effetti, no. Era necessario un modo per osservare le modifiche di stile su un elemento DOM. Sfortunatamente, le API della piattaforma web non offrono nulla che ti consenta di osservare i cambiamenti di stile.
MutationObserver
sarebbe una prima scelta logica, ma non funziona nella maggior parte dei casi. Ad esempio, nella demo, riceveremo un callback quando la classe sticky
viene aggiunta a un elemento, ma non quando lo stile calcolato dell'elemento cambia.
Ricorda che la classe sticky
è già stata dichiarata durante il caricamento pagina.
In futuro, potrebbe essere utile un'estensione"Style Mutation Observer" di Mutation Observer per osservare le modifiche agli stili calcolati di un elemento.
position: sticky
.