Scroll
Lección 28 de 37 · 5 min
El navegador ya sabe scrollear. Con unas pocas propiedades le decís dónde frenar, cuánto lugar dejar arriba de un ancla, qué hacer al llegar al final y cuánto espacio reservar para la barra.
Frenar en un punto
En Overflow viste que overflow: auto crea un contenedor con scroll. Con scroll-snap-type en ese contenedor y scroll-snap-align en los hijos, el scroll termina siempre con un hijo alineado, como en un carrusel.
- Tipo
scroll-snap-typeVa en el contenedor: un eje (x,y,both,inlineoblock) y una intensidad.mandatoryfrena siempre en un punto;proximity, solo si el scroll terminó cerca de uno. - Alineación
scroll-snap-alignVa en cada hijo y dice qué borde se alinea:start,centeroend. - Margen de frenado
scroll-paddingAchica la zona de alineación, el snapport: con24pxcada paso frena a 24px del borde y se asoma el anterior. Sumale el mismopadding-inline, así el primero y el último también llegan a esa línea. - Sin saltos
scroll-snap-stop: alwaysNo deja saltear hijos con un deslizamiento rápido: frena en el siguiente aunque el gesto diera para más.
Scrolleá la receta con el trackpad, el dedo o las flechas. La línea naranja es donde frena el borde alineado; la lectura de arriba dice qué paso quedó ahí y a cuántos píxeles.
Pan de campo con masa madre
6 pasos · 7 h
.receta {display: flex;overflow-x: auto;scroll-snap-type: x mandatory;scroll-padding-inline: 24px;padding-inline: 24px;}.paso {flex: none;scroll-snap-align: start;}
<div class="flex overflow-x-auto snap-x snap-mandatory scroll-px-6 px-6" tabindex="0" role="region" aria-label="Pasos de la receta"><section class="w-[72%] flex-none snap-start">…</section><section class="w-[72%] flex-none snap-start">…</section>…</div>
Con proximity el scroll puede quedar entre dos pasos; con none no hay línea, y el desvío es el que dejaste. Los botones usan scrollBy(): el snap también ordena los scrolls de JavaScript.
Es Baseline desde hace años: scroll-snap-type anda completo desde Chrome 69, Firefox 99 y Safari 11, y scroll-snap-stop desde Chrome 75, Firefox 103 y Safari 15.
Anclas bajo un encabezado fijo
Cuando un enlace salta a #limpieza, el navegador pone el título arriba de todo. Si hay un encabezado sticky, el título queda tapado. Hay dos formas de dejarle lugar:
- En el contenedor
scroll-padding-topVa en el contenedor que scrollea y vale para todos los destinos de una vez. Es la que conviene para un encabezado fijo. - En el destino
scroll-margin-topUn margen que solo cuenta al llevar el elemento a la vista. Sirve cuando uno necesita más aire que los demás.
Las dos afectan a las anclas, a scrollIntoView(), al foco que lleva un elemento a la vista y al snap. El encabezado del manual de la cafetera mide 48px: con 56px el título queda abajo, con 8px de aire.
Probalo en el manual: tocá un enlace del índice y cambiá cómo se le deja lugar al encabezado.
.manual {overflow-y: auto;scroll-padding-top: 56px;overscroll-behavior: contain;}@media (prefers-reduced-motion: no-preference) {.manual {scroll-behavior: smooth;}}.indice {position: sticky;top: 0;height: 48px;}
<div class="h-72 overflow-y-auto scroll-pt-14 overscroll-contain motion-safe:scroll-smooth" tabindex="0" role="region" aria-label="Manual de la cafetera"><nav class="sticky top-0 h-12"><a href="#molienda">Molienda</a><a href="#carga">Carga</a><a href="#fuego">Fuego</a><a href="#limpieza">Limpieza</a></nav><h3 id="molienda">Molienda</h3>…</div>
scroll-behavior: smooth anima esos saltos: las anclas, scrollTo() y scrollIntoView(). No toca el scroll de la rueda ni del dedo. Ponelo dentro de prefers-reduced-motion: no-preference, así quien pidió menos movimiento salta sin animación.
html {scroll-padding-top: 4rem; /* el alto del encabezado, más aire */}@media (prefers-reduced-motion: no-preference) {html {scroll-behavior: smooth;}}
scroll-padding y scroll-margin son Baseline desde 2021 (Chrome 69, Firefox 68, Safari 14.1). scroll-behavior es Baseline desde 2022, con Safari 15.4.
Qué pasa al llegar al final
Si scrolleás un panel hasta el final y seguís, el scroll pasa a la página. Eso es el encadenamiento de scroll. En un menú lateral o en un chat molesta: llegás al final de la lista y se te mueve todo lo de atrás.
- Encadena
autoEl comportamiento normal: pasa el scroll a la página y, en el celular, hace el rebote y el gesto de deslizar para recargar. - Corta la cadena
containEl scroll se queda en el panel, pero el rebote sigue estando adentro. - Corta y no rebota
noneCorta la cadena y saca el rebote. En elhtml,overscroll-behavior-y: nonedesactiva el deslizar para recargar del navegador.
Probalo con el manual de la cafetera: con contain, la rueda se queda en el panel al llegar a la garantía; con auto, sigue con la página.
.chat {overflow-y: auto;overscroll-behavior: contain;}/* una app con su propio gesto de recargar */html {overscroll-behavior-y: none;}
contain y none andan en Chrome 63, Firefox 59 y Safari 16, pero solo en contenedores que de verdad tienen scroll. Chrome 144 y Firefox 150 lo extendieron a contenedores sin contenido de más y Safari todavía no, por eso web-features no lo cuenta como Baseline. Para el caso de siempre, un panel que scrollea, podés usarlo tranquilo.
La barra de scroll
En Windows, en Linux y en macOS con la barra siempre visible, la barra ocupa lugar: le saca ancho al contenido. Cuando una lista crece y aparece la barra, todo se corre unos 15px a la izquierda. Con barras superpuestas (macOS por defecto, los celulares) no pasa, porque la barra flota encima.
- Reservar el lugar
scrollbar-gutter: stableGuarda el lugar de la barra aunque no haga falta, así el contenido no salta cuando aparece. Con barras superpuestas no hace nada. - Ancho
scrollbar-widthAceptaauto,thinonone.nonela esconde pero sigue scrolleando: dejá otra pista de que hay más contenido. - Color
scrollbar-colorToma dos colores: el de la barra que se arrastra y el del riel.
Con auto, compará la columna del andén en los horarios de 4 y de 12 trenes. Después probá con stable. La franja naranja es el ancho que la barra le saca al contenido; con barras superpuestas mide 0.
Retiro → Tigre
4 trenes
Retiro → Tigre
12 trenes
.horarios {overflow-y: auto;scrollbar-gutter: auto;scrollbar-width: auto;}
<div class="h-49 overflow-y-auto" tabindex="0" role="region" aria-label="Horarios de los sábados"><table>…</table></div>
Son más nuevas: scrollbar-gutter y scrollbar-width son Baseline 2024 y scrollbar-color es Baseline 2025, con Safari 26.2. Si un navegador no las conoce, dibuja la barra de siempre: no se rompe nada.
En Tailwind
Tailwind tiene clases para el snap, el padding y el margin de scroll, el scroll suave y el overscroll. Para la barra no hay: van como propiedades arbitrarias entre corchetes.
| CSS | Tailwind |
|---|---|
| scroll-snap-type: x mandatory | snap-x snap-mandatory |
| scroll-snap-align: start | snap-start |
| scroll-snap-stop: always | snap-always |
| scroll-padding-inline: 24px | scroll-px-6 |
| scroll-padding-top: 56px | scroll-pt-14 |
| scroll-behavior: smooth | motion-safe:scroll-smooth |
| overscroll-behavior: contain | overscroll-contain |
| scrollbar-gutter: stable | [scrollbar-gutter:stable] |
| scrollbar-width: thin | [scrollbar-width:thin] |
<html class="scroll-pt-16 motion-safe:scroll-smooth"><header class="sticky top-0 h-14">…</header><!-- carrusel: frena en cada paso, a 24px del borde --><div class="flex overflow-x-auto snap-x snap-mandatoryscroll-px-6 overscroll-x-contain"tabindex="0" role="region" aria-label="Pasos"><section class="w-[72%] flex-none snap-start">…</section></div><!-- lista que crece sin correr el contenido --><div class="h-56 overflow-y-auto [scrollbar-gutter:stable]">…</div></html>
Scrolling
Lesson 28 of 37 · 5 min
The browser already knows how to scroll. A few properties tell it where to stop, how much room to leave above an anchor, what to do at the end, and how much space to keep for the scrollbar.
Stopping at a point
In Overflow you saw that overflow: auto makes a scroll container. Add scroll-snap-type to that container and scroll-snap-align to its children, and scrolling always ends with a child lined up, like a carousel.
- Type
scroll-snap-typeGoes on the container: an axis (x,y,both,inlineorblock) and a strictness.mandatoryalways stops at a point;proximityonly does when the scroll ended near one. - Alignment
scroll-snap-alignGoes on each child and says which edge lines up:start,centerorend. - Stopping margin
scroll-paddingShrinks the area children align to, the snapport: with24pxeach step stops 24px from the edge and the previous one peeks in. Add the samepadding-inline, so the first and last steps can reach that line too. - No skipping
scroll-snap-stop: alwaysWon't let a fast swipe skip children: it stops at the next one even if the gesture had room for more.
Scroll the recipe with a trackpad, a finger or the arrow keys. The orange line is where the aligned edge stops; the readout at the top says which step landed there and how many pixels away.
Sourdough country loaf
6 steps · 7 h
.recipe {display: flex;overflow-x: auto;scroll-snap-type: x mandatory;scroll-padding-inline: 24px;padding-inline: 24px;}.step {flex: none;scroll-snap-align: start;}
<div class="flex overflow-x-auto snap-x snap-mandatory scroll-px-6 px-6" tabindex="0" role="region" aria-label="Recipe steps"><section class="w-[72%] flex-none snap-start">…</section><section class="w-[72%] flex-none snap-start">…</section>…</div>
With proximity the scroll can rest between two steps; with none there's no line and the offset is wherever you left it. The buttons use scrollBy(): snapping applies to JavaScript scrolls too.
It has been Baseline for years: scroll-snap-type works fully since Chrome 69, Firefox 99 and Safari 11, and scroll-snap-stop since Chrome 75, Firefox 103 and Safari 15.
Anchors under a sticky header
When a link jumps to #cleaning, the browser puts the heading at the very top. With a sticky header, the heading ends up underneath it. There are two ways to make room:
- On the container
scroll-padding-topGoes on the scroll container and covers every target at once. It's the one to use for a fixed header. - On the target
scroll-margin-topA margin that only counts when bringing the element into view. Useful when one needs more room than the rest.
Both apply to anchors, to scrollIntoView(), to focus bringing an element into view, and to snapping. The coffee maker manual's header is 48px tall: at 56px the heading lands right below it, with 8px to spare.
Try it on the manual: press a link in the index and change how the header gets its room.
.manual {overflow-y: auto;scroll-padding-top: 56px;overscroll-behavior: contain;}@media (prefers-reduced-motion: no-preference) {.manual {scroll-behavior: smooth;}}.index {position: sticky;top: 0;height: 48px;}
<div class="h-72 overflow-y-auto scroll-pt-14 overscroll-contain motion-safe:scroll-smooth" tabindex="0" role="region" aria-label="Coffee maker manual"><nav class="sticky top-0 h-12"><a href="#grind">Grind</a><a href="#filling">Filling</a><a href="#heat">Heat</a><a href="#cleaning">Cleaning</a></nav><h3 id="grind">Grind</h3>…</div>
scroll-behavior: smooth animates those jumps: anchors, scrollTo() and scrollIntoView(). It doesn't touch wheel or touch scrolling. Put it inside prefers-reduced-motion: no-preference, so people who asked for less motion jump without the animation.
html {scroll-padding-top: 4rem; /* the header's height, plus some room */}@media (prefers-reduced-motion: no-preference) {html {scroll-behavior: smooth;}}
scroll-padding and scroll-margin have been Baseline since 2021 (Chrome 69, Firefox 68, Safari 14.1). scroll-behavior has been Baseline since 2022, with Safari 15.4.
What happens at the end
Scroll a panel to the end and keep going, and the page starts scrolling. That's scroll chaining. In a side menu or a chat it gets in the way: you reach the end of the list and everything behind it moves.
- Chains
autoThe default: it hands the scroll to the page and, on phones, does the bounce and the pull-to-refresh gesture. - Breaks the chain
containThe scroll stays in the panel, but the bounce still happens inside it. - Breaks it, no bounce
noneBreaks the chain and drops the bounce. Onhtml,overscroll-behavior-y: noneturns off the browser's pull-to-refresh.
Try it on the coffee maker manual: with contain, the wheel stays in the panel when you reach the warranty; with auto, it carries on with the page.
.chat {overflow-y: auto;overscroll-behavior: contain;}/* an app with its own pull-to-refresh */html {overscroll-behavior-y: none;}
contain and none work in Chrome 63, Firefox 59 and Safari 16, but only on containers that actually scroll. Chrome 144 and Firefox 150 extended it to containers with nothing to scroll and Safari hasn't yet, so web-features doesn't count it as Baseline. For the usual case, a panel that scrolls, it's safe to use.
The scrollbar
On Windows, Linux, and macOS with scrollbars always shown, the scrollbar takes room: it eats into the content's width. When a list grows and the bar appears, everything shifts about 15px to the left. Overlay scrollbars (macOS by default, phones) don't do this, because they float on top.
- Keep the room
scrollbar-gutter: stableSaves the bar's room even when it isn't needed, so content doesn't jump when it shows up. With overlay scrollbars it does nothing. - Width
scrollbar-widthTakesauto,thinornone.nonehides it but still scrolls: leave another hint that there's more content. - Color
scrollbar-colorTakes two colors: the draggable thumb and the track.
With auto, compare the platform column in the 4-train and the 12-train timetables. Then try stable. The orange band is the width the bar takes from the content; with overlay scrollbars it measures 0.
Retiro → Tigre
4 trains
Retiro → Tigre
12 trains
.timetable {overflow-y: auto;scrollbar-gutter: auto;scrollbar-width: auto;}
<div class="h-49 overflow-y-auto" tabindex="0" role="region" aria-label="Saturday timetable"><table>…</table></div>
These are newer: scrollbar-gutter and scrollbar-width are Baseline 2024 and scrollbar-color is Baseline 2025, with Safari 26.2. A browser that doesn't know them draws its usual bar: nothing breaks.
In Tailwind
Tailwind has classes for snapping, scroll padding and margin, smooth scrolling and overscroll. There are none for the scrollbar: those go in as arbitrary properties in brackets.
| CSS | Tailwind |
|---|---|
| scroll-snap-type: x mandatory | snap-x snap-mandatory |
| scroll-snap-align: start | snap-start |
| scroll-snap-stop: always | snap-always |
| scroll-padding-inline: 24px | scroll-px-6 |
| scroll-padding-top: 56px | scroll-pt-14 |
| scroll-behavior: smooth | motion-safe:scroll-smooth |
| overscroll-behavior: contain | overscroll-contain |
| scrollbar-gutter: stable | [scrollbar-gutter:stable] |
| scrollbar-width: thin | [scrollbar-width:thin] |
<html class="scroll-pt-16 motion-safe:scroll-smooth"><header class="sticky top-0 h-14">…</header><!-- carousel: stops at every step, 24px from the edge --><div class="flex overflow-x-auto snap-x snap-mandatoryscroll-px-6 overscroll-x-contain"tabindex="0" role="region" aria-label="Steps"><section class="w-[72%] flex-none snap-start">…</section></div><!-- a list that grows without shifting the content --><div class="h-56 overflow-y-auto [scrollbar-gutter:stable]">…</div></html>