Saltar al contenido
scroll.css · devschool

Scroll en CSS

Lección 36 de 50 · 11 min de lectura · Actualizado el

En esta lección
  1. Repaso: ¿cuándo aparece el scroll?
  2. Desplazamiento suave: scroll-behavior
  3. Anclas y cabeceras fijas: scroll-margin-top
  4. Carruseles con scroll-snap
  5. Al llegar al final: overscroll-behavior
  6. Elementos pegajosos: position: sticky
  7. Evitar saltos con la barra: scrollbar-gutter
  8. Dar estilo a la barra de desplazamiento
  9. Animaciones ligadas al scroll
  10. Desplazar desde JavaScript
  11. Errores frecuentes
  12. Resumen

El scroll (desplazamiento) es la forma en que nos movemos por casi todas las webs, sobre todo en el móvil. Durante años, controlarlo exigía JavaScript: desplazamientos suaves, carruseles que encajan en cada foto, cabeceras que se quedan pegadas. Hoy CSS hace todo eso por sí solo, con menos código y mejor rendimiento. En esta lección aprenderás a suavizar los saltos a anclas, a evitar que una cabecera fija tape los títulos, a crear carruseles con scroll-snap, a controlar qué pasa al llegar al final de una zona con scroll y a dar estilo a la barra de desplazamiento.

Repaso: ¿cuándo aparece el scroll?

Hay scroll cuando el contenido no cabe en su caja. La página entera tiene scroll porque normalmente es más alta que la ventana; una caja concreta lo tiene si le das un tamaño y le pones overflow: auto:

.lista-mensajes {
  height: 300px;
  overflow-y: auto;   /* barra vertical solo si hace falta */
}

Los valores de overflow (visible, hidden, scroll, auto) los tienes explicados en display y overflow. A esa caja con scroll propio se la llama contenedor de scroll, y muchas de las propiedades de esta lección se aplican a ella. Cuando se trata de la página entera, el contenedor de scroll es el elemento <html>.

Desplazamiento suave: scroll-behavior

Cuando pulsas un enlace a un ancla (<a href="#contacto">), el navegador salta de golpe a esa sección. Con una línea, el salto se convierte en un desplazamiento animado:

html {
  scroll-behavior: smooth;
}

Se pone en el contenedor de scroll: en html para la página, o en una caja con overflow: auto para su contenido. También afecta a los desplazamientos que hagas desde JavaScript con scrollTo() o scrollIntoView().

Al usuario que ha pedido menos movimiento en su sistema (por mareos o problemas vestibulares), el desplazamiento suave le puede molestar. Lo correcto es activarlo solo si no ha pedido lo contrario:

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

Tienes más sobre esta preferencia en CSS accesible.

Anclas y cabeceras fijas: scroll-margin-top

Un problema muy común: tu web tiene una cabecera fija o pegajosa de 64 px. Pulsas el enlace “Precios” y la página baja hasta la sección… pero el título queda escondido debajo de la cabecera, porque el navegador coloca la sección justo en el borde superior de la ventana.

La solución es decirle a cada destino que deje un margen al llegar:

.cabecera {
  position: sticky;
  top: 0;
  height: 64px;
}

section[id] {
  scroll-margin-top: 80px;   /* 64 px de cabecera + 16 de aire */
}

scroll-margin-top no cambia nada del diseño normal: solo se usa cuando el navegador desplaza la página hasta ese elemento. Hay versiones para cada lado (scroll-margin-bottom, scroll-margin-inline…) y la abreviada scroll-margin.

Existe la alternativa de ponerlo una vez en el contenedor de scroll con scroll-padding-top:

html {
  scroll-padding-top: 80px;
}

La diferencia: scroll-padding se pone en el contenedor y vale para todos los destinos; scroll-margin se pone en cada destino. Para una cabecera fija, scroll-padding-top en html es lo más cómodo.

Consejo: si la altura de la cabecera cambia en el móvil, guárdala en una variable CSS (--alto-cabecera) y úsala tanto en la cabecera como en scroll-padding-top.

Carruseles con scroll-snap

Un carrusel de fotos en el móvil se desliza con el dedo. Sin ayuda, se queda parado donde lo sueltes, a menudo con una foto cortada por la mitad. Scroll snap (encaje del scroll) hace que el desplazamiento se “imante” a cada elemento.

Necesitas dos cosas:

  1. En el contenedor: scroll-snap-type, que indica el eje y la fuerza del encaje.
  2. En los hijos: scroll-snap-align, que indica qué parte de cada hijo debe encajar.
<div class="carrusel">
  <img src="ruta-1.jpg" alt="Lago de la Ercina">
  <img src="ruta-2.jpg" alt="Cola de Caballo en Ordesa">
  <img src="ruta-3.jpg" alt="Laguna de las Yeguas">
</div>
.carrusel {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

.carrusel img {
  flex: 0 0 85%;           /* cada foto ocupa el 85 % y asoma la siguiente */
  scroll-snap-align: center;
  object-fit: cover;
}

Valores de scroll-snap-type:

ValorEfecto
x mandatoryEje horizontal; siempre encaja en un elemento al soltar
x proximityEncaja solo si sueltas cerca de un punto de encaje
y mandatoryIgual, en vertical (secciones a pantalla completa)
both mandatoryEn los dos ejes

Valores de scroll-snap-align: start (encaja el borde inicial), center o end.

Con mandatory el usuario nunca se queda a medias, pero si un hijo es más alto que el contenedor (en vertical), puede resultar imposible leerlo entero. En ese caso usa proximity.

Dos detalles más:

  • scroll-snap-stop: always en los hijos impide que un deslizamiento rápido se salte elementos: avanza de uno en uno.
  • scroll-padding en el contenedor ajusta dónde encaja (por ejemplo, para dejar un margen a la izquierda).

Que el ancho de cada foto sea algo menor del 100 % no es casualidad: si asoma la siguiente foto, el usuario entiende que puede deslizar.

Secciones a pantalla completa

Para una presentación o una portada en la que cada sección ocupa toda la pantalla:

.presentacion {
  height: 100dvh;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
}

.presentacion > section {
  height: 100dvh;
  scroll-snap-align: start;
}

Úsalo con moderación: secuestrar el scroll de toda una web suele molestar.

Al llegar al final: overscroll-behavior

Imagina un chat en una caja con scroll dentro de una página larga. Bajas hasta el último mensaje, sigues deslizando… y empieza a moverse la página de detrás. Este efecto se llama encadenamiento del scroll (scroll chaining). En el móvil también provoca el “tirar para recargar” o el rebote de la página.

overscroll-behavior decide qué pasa cuando un contenedor llega a su límite:

.chat {
  height: 400px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
ValorQué hace
autoComportamiento normal: el scroll pasa al padre
containEl scroll no pasa al padre, pero se mantienen efectos como el rebote dentro de la caja
noneComo contain y además sin rebote ni “tirar para recargar”

Es muy útil en menús laterales, ventanas modales con scroll, chats y carruseles (overscroll-behavior-x: contain evita que deslizar el carrusel active el gesto de “atrás” del navegador en algunos móviles).

Elementos pegajosos: position: sticky

position: sticky hace que un elemento se desplace normalmente hasta llegar a cierta posición, y a partir de ahí se quede pegado mientras su contenedor siga visible. Lo tienes explicado en posicionamiento; aquí vas a ver dos usos típicos relacionados con el scroll.

Cabeceras de grupo en una lista

En una agenda de contactos, cada letra se queda arriba mientras recorres sus contactos, y la siguiente letra la “empuja” al llegar:

<div class="agenda">
  <section>
    <h3>A</h3>
    <p>Alba</p> <p>Andrés</p> <p>Aitana</p>
  </section>
  <section>
    <h3>B</h3>
    <p>Bruno</p> <p>Berta</p>
  </section>
</div>
.agenda {
  height: 300px;
  overflow-y: auto;
}

.agenda h3 {
  position: sticky;
  top: 0;
  margin: 0;
  background: #e8eef8;   /* sin fondo, el texto de debajo se vería a través */
}

Cada <h3> está pegado dentro de su <section>: cuando la sección termina, la cabecera se va con ella. Por eso la siguiente letra parece empujar a la anterior.

Columnas laterales y cabeceras de tabla

/* Índice lateral que te acompaña al leer un artículo largo */
.indice {
  position: sticky;
  top: 80px;               /* debajo de la cabecera fija */
  align-self: start;       /* imprescindible si el padre es flex o grid */
}

/* Encabezados de una tabla larga */
thead th {
  position: sticky;
  top: 0;
  background: white;
}

Cuidado: sticky deja de funcionar si algún antepasado tiene overflow: hidden (o auto) sin necesitarlo, porque entonces el elemento se pega dentro de ese antepasado y no de la página. Si tu sticky “no hace nada”, revisa el overflow de los padres en DevTools.

Evitar saltos con la barra: scrollbar-gutter

En Windows, la barra de desplazamiento ocupa espacio (unos 15 px). Si una página es corta no hay barra, y si es larga sí; al navegar entre ellas, todo el contenido centrado da un pequeño salto lateral. Lo mismo pasa al abrir una modal que bloquea el scroll.

scrollbar-gutter reserva el hueco de la barra aunque no haga falta:

html {
  scrollbar-gutter: stable;
}

Con stable both-edges reserva el mismo hueco también en el lado contrario, para que el contenido quede simétrico. En macOS y en móviles la barra suele flotar sobre el contenido y no ocupa espacio, así que allí no notarás diferencia.

Dar estilo a la barra de desplazamiento

Hay dos propiedades estándar, sencillas y suficientes para la mayoría de casos:

.panel-lateral {
  overflow-y: auto;
  scrollbar-width: thin;                 /* auto, thin o none */
  scrollbar-color: #2d6cdf #e8eef8;      /* color del "pulgar" y del carril */
}
  • scrollbar-width: thin hace la barra más fina; none la oculta (el contenido sigue pudiendo desplazarse).
  • scrollbar-color: dos colores, primero el de la parte que se arrastra (el pulgar) y después el del carril.

Verás en código antiguo los pseudoelementos ::-webkit-scrollbar, ::-webkit-scrollbar-thumb… Son propios de los navegadores basados en Chrome y Safari, no son estándar y permiten más detalles (bordes redondeados, sombras). Hoy, en navegadores que entienden las dos propiedades estándar, estas tienen prioridad.

Cuidado: ocultar la barra con scrollbar-width: none hace que el usuario no sepa que hay más contenido. Hazlo solo cuando haya otra pista visual, como un carrusel en el que asoma la siguiente foto.

Animaciones ligadas al scroll

Los navegadores modernos permiten conectar una animación CSS al progreso del scroll en lugar de al tiempo: una barra de lectura que se llena al bajar o imágenes que aparecen al entrar en pantalla, sin JavaScript. Se hace con animation-timeline: scroll() y animation-timeline: view(). Lo verás con detalle en animaciones, porque primero necesitas dominar @keyframes.

Desplazar desde JavaScript

A veces el desplazamiento lo provoca un botón (“Volver arriba”, “Ir al formulario”). Con scroll-behavior: smooth en CSS, estos métodos se animan solos:

const botonArriba = document.querySelector('#arriba');
botonArriba.addEventListener('click', () => {
  window.scrollTo({ top: 0 });
});

const botonReservar = document.querySelector('#ir-reserva');
botonReservar.addEventListener('click', () => {
  document.querySelector('#reserva').scrollIntoView();
});

scrollIntoView() también respeta scroll-margin-top, así que el destino no queda bajo la cabecera. Si quieres forzar la animación desde JavaScript sin depender del CSS, pasa { behavior: 'smooth' }.

Errores frecuentes

  • Poner scroll-behavior en body. En la mayoría de páginas el contenedor de scroll es html. Ponlo en html.
  • Títulos tapados por la cabecera fija. Añade scroll-padding-top en html o scroll-margin-top en los destinos.
  • scroll-snap-align en el contenedor. Va en los hijos; en el contenedor va scroll-snap-type.
  • Carrusel sin overflow-x: auto. Sin contenedor de scroll no hay nada que encajar: los hijos se encogen o se salen.
  • Sticky que no se pega. Le falta top, algún padre tiene overflow o, dentro de Flexbox o Grid, se estira a toda la altura (añade align-self: start).
  • Sticky transparente. Sin background, el contenido que pasa por debajo se ve a través.
  • Ocultar la barra sin dar pistas de que hay más contenido.
  • Olvidar prefers-reduced-motion al activar el desplazamiento suave.

Resumen

PropiedadDóndePara qué
overflow: autoContenedorCrear un contenedor de scroll
scroll-behavior: smoothhtml o contenedorDesplazamiento animado a anclas
scroll-padding-tophtml o contenedorMargen para todos los destinos (cabecera fija)
scroll-margin-topDestinoMargen para un destino concreto
scroll-snap-typeContenedorActivar el encaje (x mandatory, y proximity…)
scroll-snap-alignHijosQué parte encaja (start, center, end)
overscroll-behaviorContenedorQue el scroll no pase al padre
position: stickyElementoPegarse al hacer scroll
scrollbar-gutter: stablehtml o contenedorReservar el hueco de la barra
scrollbar-width / scrollbar-colorContenedorGrosor y colores de la barra

Muchas de estas decisiones afectan a cómo usan la web las personas con distintas necesidades. Sigue con CSS accesible.

Pruébalo tú

Cambia el código y pulsa Ejecutar (o Ctrl + Enter).

scroll.html
Resultado

Pon a prueba lo que has aprendido

[CSS] ¿Dónde se pone scroll-behavior: smooth normalmente?

[CSS] En un carrusel con scroll-snap, ¿dónde va scroll-snap-align?

[CSS] Un elemento con position: sticky no se pega. ¿Qué causa es habitual?

[CSS] ¿Qué evita overscroll-behavior: contain en un panel con scroll propio?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.