Scroll en CSS
En esta lección
- Repaso: ¿cuándo aparece el scroll?
- Desplazamiento suave: scroll-behavior
- Anclas y cabeceras fijas: scroll-margin-top
- Carruseles con scroll-snap
- Al llegar al final: overscroll-behavior
- Elementos pegajosos: position: sticky
- Evitar saltos con la barra: scrollbar-gutter
- Dar estilo a la barra de desplazamiento
- Animaciones ligadas al scroll
- Desplazar desde JavaScript
- Errores frecuentes
- 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 enscroll-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:
- En el contenedor:
scroll-snap-type, que indica el eje y la fuerza del encaje. - 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:
| Valor | Efecto |
|---|---|
x mandatory | Eje horizontal; siempre encaja en un elemento al soltar |
x proximity | Encaja solo si sueltas cerca de un punto de encaje |
y mandatory | Igual, en vertical (secciones a pantalla completa) |
both mandatory | En 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: alwaysen los hijos impide que un deslizamiento rápido se salte elementos: avanza de uno en uno.scroll-paddingen 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;
}
| Valor | Qué hace |
|---|---|
auto | Comportamiento normal: el scroll pasa al padre |
contain | El scroll no pasa al padre, pero se mantienen efectos como el rebote dentro de la caja |
none | Como 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:
stickydeja de funcionar si algún antepasado tieneoverflow: hidden(oauto) 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 eloverflowde 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:thinhace la barra más fina;nonela 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: nonehace 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-behaviorenbody. En la mayoría de páginas el contenedor de scroll eshtml. Ponlo enhtml. - Títulos tapados por la cabecera fija. Añade
scroll-padding-topenhtmloscroll-margin-topen los destinos. scroll-snap-alignen el contenedor. Va en los hijos; en el contenedor vascroll-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 tieneoverflowo, dentro de Flexbox o Grid, se estira a toda la altura (añadealign-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-motional activar el desplazamiento suave.
Resumen
| Propiedad | Dónde | Para qué |
|---|---|---|
overflow: auto | Contenedor | Crear un contenedor de scroll |
scroll-behavior: smooth | html o contenedor | Desplazamiento animado a anclas |
scroll-padding-top | html o contenedor | Margen para todos los destinos (cabecera fija) |
scroll-margin-top | Destino | Margen para un destino concreto |
scroll-snap-type | Contenedor | Activar el encaje (x mandatory, y proximity…) |
scroll-snap-align | Hijos | Qué parte encaja (start, center, end) |
overscroll-behavior | Contenedor | Que el scroll no pase al padre |
position: sticky | Elemento | Pegarse al hacer scroll |
scrollbar-gutter: stable | html o contenedor | Reservar el hueco de la barra |
scrollbar-width / scrollbar-color | Contenedor | Grosor 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).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.