IntersectionObserver - Observa Elementos HTML cuando son visibles mediante el Scroll en JavaScript

Video thumbnail

El IntersectionObserver es una de esas APIs de JavaScript que, cuando la descubres, te preguntas cómo sobrevivías usando solo scroll events. En mi caso, me permitió automatizar algo que siempre hacía manualmente: detectar qué título está visible en un post y activar el enlace correspondiente en el índice.
Desde entonces, no he vuelto atrás.

En esta guía te explico cómo funciona, cómo configurarlo bien y cómo aplicarlo a un caso real de navegación interna (scroll-spy) usando exactamente el mismo patrón que uso yo en mis proyectos.

Te voy a explicar un método muy interesante que se conoce como Intersection Observer en JavaScript. Básicamente, me permite implementar este tipo de comportamiento: agregar un observable sobre un listado de IDs en un visor e ir marcando como activado un enlace en base a un titulo que se encuentre visible en el viewport.

Qué es IntersectionObserver y por qué facilita la detección de visibilidad

IntersectionObserver es una API que te permite ejecutar una función cuando un elemento entra o sale del viewport (o de cualquier contenedor que tú definas).
Y lo hace sin usar eventos de scroll constantes, lo que significa:

  • mejor rendimiento,
  • código más limpio,
  • y detecciones más precisas.

Antes había que medir posiciones con getBoundingClientRect() y eso se repetía en cada scroll. Ahora el navegador hace el trabajo sucio por ti.

¿Qué puedes detectar con él?

  • Si una imagen está a punto de aparecer → lazy-loading.
  • Si un bloque está ya visible → animaciones o estadísticas.
  • Si un elemento está 100% dentro de la vista → activar una sección.
  • Si un título entra al 60% de visibilidad → como hago yo, activar un enlace del índice.

Conceptos clave: root, rootMargin, threshold y entries

Aunque la API es sencilla, conviene interiorizar estos cuatro conceptos porque son la base de todo.

root

Es el contenedor cuya visibilidad usarás como referencia.

  • Si es null: el viewport del navegador.
  • Si lo defines: cualquier div scrollable.

rootMargin

Es como el margin del CSS pero aplicado al área visible del root.
Permite “adelantar” o “atrasar” la zona de detección.

Yo, por ejemplo, uso rootMargin: '0px 0px -60% 0px' porque quiero que un título se considere visible cuando ya está más arriba del viewport, no justo cuando aparece.

threshold

Es el porcentaje de visibilidad que debe alcanzar el elemento para que se dispare el callback.
Puede ser:

  • un número (0.6 por ejemplo),
  • o un array de números ([0, 0.25, 0.5, 1]).

Por ejemplo:

  • threshold: 1 → solo cuando el elemento esté totalmente visible
  • threshold: 0 → en cuanto toque el borde
  • threshold: 0.6 → este es el caso que suelo usar

entries

Es un array que recibe tu callback cada vez que haya cambios.
Cada entrada contiene:

  • entry.target: elemento observado
  • entry.isIntersecting: si está intersectando
  • entry.intersectionRatio: porcentaje visible
  • entry.boundingClientRect
  • entry.intersectionRect
  • entry.rootBounds

Te permite decidir exactamente qué quieres hacer cuando un elemento se cruza con tu zona objetivo.

¿Cómo funciona Intersection Observer?

1. Definir las opciones

const options = {
 root: null,
 rootMargin: '0px',
 threshold: 0.5,
};

2. Crear el observador

const observer = new IntersectionObserver((entries) => {
 entries.forEach(entry => {
   if (entry.isIntersecting) {
     console.log(entry.target.id);
   }
 });
}, options);

3. Seleccionar los elementos

const elementos = document.querySelectorAll('.observado');

4. Observarlos

elementos.forEach(el => observer.observe(el));

Ejemplo práctico: activar enlaces según el título visible

Este es exactamente el caso donde yo más uso esta API.
Cuando escribo un artículo con muchos títulos <h2> y <h3>, me viene genial detectar cuál está visible y activar el enlace equivalente del índice lateral.

1. Seleccionar los títulos del post

const headings = Array.from(
 document.querySelectorAll('.post h2, .post h3')
);

Esto asegura que todos mis títulos principales están vigilados para lo que es el visor del libro.

2. Ajustar la zona de visibilidad

Aquí es donde entra mi experiencia práctica:

En mis proyectos considero que un título está “activo” cuando un 60% ya ha pasado por el viewport, así que uso un rootMargin negativo para mover el trigger hacia arriba.

Considero observable cuando al menos un 60% del contenido está visible en el viewport:

const observerOptions = {
  rootMargin: '0px 0px -60% 0px', // Detectar cuando el título está más arriba
};

Ese valor puedes ajustarlo a lo que te funcione mejor.

Seleccionar los elementos a observar:

const headings = Array.from(document.querySelectorAll('.post h2, .post h3'));

3. Crear el observador

En mi documento HTML (por ejemplo, en un post), quiero observar los elementos h2 y h3.

Es decir, cuando alguno de esos títulos alcanza ese 60% visible, se ejecuta el código deseado.

Luego, recorremos esos elementos con un forEach y aplicamos el .observe() para empezar a vigilarlos:

const observer = new IntersectionObserver((entries) => {

  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const id = entry.target.id;

      // Marcar el enlace activo
      this.hxs.forEach(link => {
        if (link.id === `${id}`) {
          // TO DO
        }
        
      });
    }
  });
}, observerOptions);

En mi caso:

  • Verifico si hay entradas en el array entries.
  • Si las hay, obtengo el id del título.
  • Busco el enlace equivalente.
    • Con ese id, hago lo que necesite.
  • Le aplico un estilo activo.

4. Observar cada encabezado

Antes de usarlo, necesitamos registrar el observador. De momento, solo hemos definido:

headings.forEach(h => observer.observe(h));

¿Por qué esta técnica funciona tan bien?

  • Evita parpadeos.
  • No depende del scroll, sino de visibilidad real.
  • Es ultra eficiente.
  • Detecta títulos aunque salten rápido por el viewport.

Consejos avanzados y buenas prácticas

  • Evita observar demasiados elementos sin necesidad
    • La API es eficiente, pero no abuses.
    • Observa solo lo que realmente necesitas.
  • Usa arrays de threshold cuando quieras animaciones suaves
    • Si necesitas detectar incrementos dinámicos de visibilidad:
    • threshold: [0, 0.1, 0.2, ..., 1]
  • Cuidado con isIntersecting
    • En algunos navegadores no se comporta igual si el threshold no incluye 0.
    • Si quieres precisión total cuando un elemento está completamente visible, mejor usa intersectionRatio.
  • No uses IntersectionObserver para lógica compleja en cada frame
    • Si necesitas animaciones por pixel, mejor requestAnimationFrame().

Preguntas frecuentes

  • ¿IntersectionObserver consume menos que escuchar scroll?
    • Sí, muchísimo menos. La detección no depende de eventos continuos.
  • ¿Puedo observar muchos elementos a la vez?
    • Sí. Un solo observer puede manejar cientos de elementos.
  • ¿Qué diferencia hay entre threshold e intersectionRatio?
    • threshold es cuando quieres que el callback se dispare.
      intersectionRatio es el porcentaje real visible en cada momento.
  • ¿Sirve para un menú tipo scroll-spy?
    • Sí, de hecho es mi caso práctico favorito.

Conclusión

IntersectionObserver es una herramienta potentísima y muy infravalorada ya que su uso no es muy amigable y es algo complejo de entender aparte de abstracta; aunque espero que esta guía te permita entenderlo de una manera más fácil y poder verle en potencial y usarlo en tus proyectos,

Para mi caso en particular, usarlo para activar enlaces según el título visible ha simplificado muchísimo la navegación de mis artículos: cero cálculos, cero scroll listeners, máxima precisión.

Acepto recibir anuncios de interes sobre este Blog.

"¿Qué es IntersectionObserver en JavaScript? Explicado con claridad", Scroll inteligente, La API que cambiará tu forma de hacer scroll en la web.

| 👤 Andrés Cruz

🇺🇸 In english