The values
overflow: auto; /* both */
overflow: hidden auto; /* x then y */
overflow-x: auto;
overflow-y: hidden;
| Value | What happens |
|---|---|
visible | Content spills out and is still painted. The default. |
hidden | Clipped. Still scrollable by script and by focus, which is sometimes a surprise. |
clip | Clipped, with no scrolling at all — not even programmatic. |
scroll | A scrollbar always, whether it is needed or not. |
auto | A scrollbar only when it is needed. The one you usually want. |
overflow-x: visible; overflow-y: auto is not possible: setting one axis to a clipping value forces the other to auto. If a dropdown is being cut off, the scroll container is the reason.
A scroll container changes several things
Giving an element an overflow value other than visible makes it a scroll container, and that has consequences beyond the scrollbar:
- It becomes a block formatting context — floats are contained and margins stop collapsing through it.
- It becomes the containing block for
position: stickyinside it. A sticky header inside anoverflow: hiddenwrapper sticks to that wrapper, not the page. - Anything absolutely positioned inside is clipped by it.
- It gets its own scroll position, which the browser may restore on navigation.
The horizontal scrollbar hunt
One element slightly wider than the viewport makes the whole page scroll sideways. The usual causes:
width: 100vwon a page that scrolls vertically —100vwincludes the scrollbar. Use100%.- A negative margin or a
translatepushing something past the right edge. - A long unbroken string — a URL, a hash, a code sample.
- A flex or grid item without
min-width: 0, refusing to shrink below its content. - A fixed-width element wider than a small screen.
[...document.querySelectorAll("*")]
.filter(el => el.getBoundingClientRect().right > document.documentElement.clientWidth + 1)
* { outline: 1px solid rgb(255 0 0 / .3); }
Long words and URLs
overflow-wrap: break-word is the safe default for body text. word-break: break-all breaks mid-word even when it did not have to, which reads badly in prose.
.prose {
overflow-wrap: break-word; /* break a word only if it cannot fit at all */
hyphens: auto; /* needs the right lang on the element */
}
.code {
word-break: break-all; /* break anywhere — for code and hashes only */
}
Scrolling a table
tabindex="0" is not decoration: a scroll container that a keyboard cannot reach is content a keyboard user cannot read. The role and label make it announce what it is.
<div class="tablewrap" tabindex="0" role="region" aria-label="Lead times">
<table>…</table>
</div>
.tablewrap {
overflow-x: auto;
overscroll-behavior-x: contain; /* do not chain to the page */
}
Scroll snap
mandatory always lands on a snap point; proximity only snaps when you are close. Prefer proximity for long lists, where mandatory can make it impossible to stop in between.
.rail {
display: flex;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 1rem;
}
.rail > * {
flex: none;
width: min(80%, 24rem);
scroll-snap-align: start;
}
Overscroll and smooth scrolling
.drawer {
overflow-y: auto;
overscroll-behavior: contain; /* scrolling past the end does not scroll the page */
}
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
}
overscroll-behavior: contain is what stops the page underneath a modal from scrolling when the modal reaches its end — the problem people used to solve by locking the body.
Styling scrollbars
scrollbar-gutter: stable on html is worth adding to most sites: without it, a page that gains a scrollbar shifts its whole layout sideways by 15 pixels.
.panel {
scrollbar-width: thin;
scrollbar-color: rgb(255 255 255 / .22) transparent;
scrollbar-gutter: stable; /* reserve the space so content does not shift */
}