A modo de resumen
Aquí hay un secreto: Es posible que no necesites los eventos de scroll
en tu próxima app. Con un
IntersectionObserver
,
Te muestro cómo puedes activar un evento personalizado cuando los elementos position:sticky
se corrigen o cuando dejan de pegarse. Todo esto sin la
usar objetos de escucha de desplazamiento. Incluso hay una demostración increíble que lo prueba:
Presentamos el evento sticky-change
Una de las limitaciones prácticas de usar la posición fija de CSS es que No proporciona un indicador de la plataforma para saber cuándo la propiedad está activa. En otras palabras, no hay ningún evento que sepa cuándo un elemento se vuelve fijo o cuándo deja de ser atractivo.
Toma el siguiente ejemplo, que corrige un elemento <div class="sticky">
de 10 px del
en la parte superior de su contenedor superior:
.sticky {
position: sticky;
top: 10px;
}
¿No sería genial si el navegador te dijera cuando los elementos alcanzan ese punto?
Parece que no soy el único
que piensa así. Un indicador para position:sticky
podría desbloquear varios casos de uso:
- Aplica una sombra paralela a un banner mientras se pega.
- A medida que el usuario lea tu contenido, registra los hits de estadísticas para conocer su el progreso de un proyecto.
- A medida que el usuario se desplace por la página, actualiza un widget del contenido flotante del elemento de contenido al valor actual sección.
Con estos casos de uso en mente, elaboramos un objetivo final: crear un evento que
Se activa cuando se corrige un elemento position:sticky
. Llamémosla el
sticky-change
evento:
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 demostración usa este evento a los encabezados de una sombra paralela cuando se corrigen. También actualiza el título nuevo en la parte superior de la página.
¿Efectos de desplazamiento sin eventos de desplazamiento?
Quitemos un poco la terminología para poder referirnos a estos nombres en el resto de la publicación:
- Contenedor de desplazamiento: el área de contenido (viewport visible) que contiene lista de "entradas de blog".
- Encabezados: Es un título azul en cada sección que contiene
position:sticky
. - Secciones fijas: Son todas las secciones de contenido. El texto que se desplaza debajo de encabezados fijos.
- "Modo fijo": Cuando
position:sticky
se aplica al elemento.
Para saber qué encabezado ingresa en el “modo permanente”, necesitamos alguna forma de determinar
el desplazamiento del contenedor de desplazamiento. Eso nos daría una forma
para calcular el encabezado que se muestra actualmente. Sin embargo, esto se vuelve bastante
difícil sin eventos scroll
:) El otro problema es que
position:sticky
quita el elemento del diseño cuando se corrige.
Así que, sin los eventos de desplazamiento, perdimos la capacidad de realizar tareas relacionadas con el diseño. cálculos en los encabezados.
Cómo agregar un DOM dumby para determinar la posición de desplazamiento
En lugar de eventos scroll
, usaremos un IntersectionObserver
para
determinan cuándo los encabezados entran y salen del modo permanente. Agrega dos nodos
(también conocidos como centinelas) en cada sección fija, una en la parte superior y otra en la parte superior
en la parte inferior, servirán como puntos de referencia para determinar la posición del desplazamiento. Como estas
marcadores ingresan y salen del contenedor, su visibilidad cambia y
Intersection Observer activa una devolución de llamada.
Necesitamos dos centinelas para cubrir cuatro casos de desplazamiento hacia arriba y hacia abajo:
- Desplazamiento hacia abajo: El encabezado se vuelve fijo cuando el centinela superior se cruza. la parte superior del contenedor.
- Desplazamiento hacia abajo: header sale del modo permanente cuando llega a la parte inferior de la sección y el centinela inferior cruza la parte superior del contenedor.
- Desplazamiento hacia arriba: header sale del modo permanente cuando se desplaza el centinela superior. de nuevo en la parte superior.
- Desplazamiento hacia arriba: El encabezado se vuelve pegajoso a medida que el centinela inferior se desplaza hacia atrás. en la parte superior.
Es útil ver una presentación en pantalla de 1 a 4 en el orden en que ocurren:
El CSS
Los centinelas se colocan en la parte superior e inferior de cada sección.
.sticky_sentinel--top
se encuentra en la parte superior del encabezado mientras
.sticky_sentinel--bottom
se encuentra en la parte inferior de la sección:
: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;
}
Configura Intersection Observers
Intersection Observer observan de forma asíncrona los cambios en la intersección de un elemento de destino y el viewport del documento o un contenedor superior. En nuestro caso, vemos intersecciones con un contenedor superior.
El ingrediente mágico es IntersectionObserver
. Cada centinela recibe una
IntersectionObserver
al observador su visibilidad de la intersección dentro de
contenedor de desplazamiento. Cuando un centinela se desplaza hasta la viewport visible, sabemos
un encabezado se fija o deja de ser fijo. Del mismo modo, cuando un centinela sale
el viewport.
Primero, configuro observadores para los centinelas de encabezado y pie de página:
/**
* 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'));
Luego, agregué un observador para que se active cuando pasen elementos .sticky_sentinel--top
.
en la parte superior del contenedor de desplazamiento (en cualquier dirección).
La función observeHeaders
crea los centinelas principales y los agrega a
cada sección. El observador calcula la intersección del centinela con
del contenedor y decide si ingresa al viewport o sale de él. Que
la información determina si el encabezado de la sección se fija o no.
/**
* 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));
}
El observador está configurado con threshold: [0]
para que su devolución de llamada se active en cuanto.
a medida que el centinela se hace visible.
El proceso es similar para el centinela inferior (.sticky_sentinel--bottom
).
Se crea un segundo observador para que se active cuando los pies de página pasen por la parte inferior.
del contenedor de desplazamiento. La función observeFooters
crea la
nodos centinela y los adjunta a cada sección. El observador calcula la
intersección del centinela con la parte inferior del contenedor y decide si es
entrar o salir. Esa información determina si el encabezado de la sección
se mantienen o no.
/**
* 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));
}
El observador está configurado con threshold: [1]
para que su devolución de llamada se active cuando el elemento
todo el nodo esté a la vista.
Por último, están las dos utilidades para activar el evento personalizado sticky-change
.
y generar los centinelas:
/**
* @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);
}
Eso es todo.
Demostración final
Creamos un evento personalizado cuando los elementos con position:sticky
se convierten en
Se corrigieron y agregaron efectos de desplazamiento sin usar eventos scroll
.
Conclusión
A menudo, me pregunto si IntersectionObserver
ser una herramienta útil para reemplazar algunos de los patrones de la IU basados en eventos scroll
que
desarrollaron a lo largo de los años. Resulta que la respuesta es sí y no. La semántica
de la API de IntersectionObserver
dificultan su uso para todo. Sin embargo, como
que mostré aquí, que puedes
utilizar para algunas técnicas interesantes.
¿Otra forma de detectar cambios de estilo?
En realidad, no. Lo que necesitábamos era una manera de observar los cambios de estilo en un elemento del DOM. Lamentablemente, no hay nada en las APIs de la plataforma web que te permita cambios en el estilo del reloj.
Un MutationObserver
sería una primera opción lógica, pero eso no funciona para
en la mayoría de los casos. Por ejemplo, en la demostración, recibiremos una devolución de llamada cuando sticky
cuando se agrega la clase a un elemento, pero no cuando cambia el estilo calculado del elemento.
Recuerda que ya se declaró la clase sticky
durante la carga de la página.
En el futuro, un
"Style Mutation Observer" (Observador de mutación de estilo)
una extensión a Mutation Observers podría ser útil para observar cambios en un
los estilos calculados de un elemento.
position: sticky
.