CSS overflow: visible, hidden, scroll, auto and clip
Setting overflow-x to scroll silently changes overflow-y to auto. Scrollbars also take 15px out of your content box.

overflow decides what happens when content is bigger than the box holding it:
overflow: visible; /* default: it spills out */
overflow: hidden; /* clipped, no scrollbars */
overflow: scroll; /* clipped, scrollbars always */
overflow: auto; /* clipped, scrollbars only if needed */
overflow: clip; /* clipped, no scrolling at all */
Two things about it are not obvious and both cause real bugs: scrollbars take space out of your content box, and setting one axis changes the other.
The four values, measured
A 200 × 100 box containing 400 × 300 of content:
| Value | clientWidth | clientHeight | Scrollbar |
|---|---|---|---|
visible |
200 | 100 | 0 |
hidden |
200 | 100 | 0 |
scroll |
185 | 85 | 15px |
auto |
185 | 85 | 15px |
visible and hidden keep the full 200 × 100 content box. scroll and auto lose 15 pixels on each axis to the scrollbars.
That is the first practical point. A scrollbar is not drawn on top of your content, it is carved out of it. A layout that fits exactly at 200px will not fit once the box starts scrolling, and the failure appears only when there is enough content to trigger it.
On macOS, where overlay scrollbars are the default, the same box loses nothing until a mouse is connected. So this is a bug that reproduces on some machines and not others, which is worth knowing before you spend an hour on it.
scrollbar-gutter: stable reserves the space whether or not the scrollbar is showing, which stops content shifting sideways as it grows past the threshold. It is the fix for a page that jiggles when a modal opens.
Setting one axis changes the other
#xonly { overflow-x: scroll; }
Measured:
computed overflow-x: scroll
computed overflow-y: auto
You set one property, and overflow-y became auto on its own. It did not stay visible.
That is specified behaviour rather than a browser quirk. visible on one axis is not compatible with a scrolling value on the other, because content that scrolls horizontally cannot also spill vertically out of the same box. When you set one axis to anything other than visible, the other is computed as auto.
The consequence catches people building a horizontally-scrolling row: a dropdown or tooltip inside it is now clipped vertically, because the container gained a vertical scroll behaviour nobody asked for. The usual fix is to move the overflowing element out of the scroller, or to use clip with overflow-clip-margin where support allows.
hidden versus clip
They look identical and differ in one important way.
overflow: hidden makes the element a scroll container. Nothing shows a scrollbar, and the content is still scrollable programmatically, which means scrollTop works and anchor links can shift it.
overflow: clip simply clips. No scroll container, no programmatic scrolling.
That distinction matters because being a scroll container has side effects far from the element:
- It breaks
position: stickyin descendants. - It creates a new formatting context, which contains floats and stops margins collapsing through.
- It can be scrolled by focus, so a hidden element receiving focus can visibly shift the content.
overflow: hidden is one of the most-typed declarations in CSS, usually to clip a rounded corner or contain a float, and it drags all of that along with it. When you only need clipping, clip is the more precise tool. For containing floats specifically, display: flow-root is the purpose-built answer.
Why a page scrolls sideways
The most common overflow bug is a horizontal scrollbar on the whole page from a single element that is a few pixels too wide.
Find it in the console:
document.querySelectorAll("*").forEach((el) => {
if (el.getBoundingClientRect().right > document.documentElement.clientWidth) {
console.log(el, el.getBoundingClientRect().right);
}
});
That lists every element extending past the viewport's right edge. Usually there is exactly one, and it is one of:
- A
width: 100%element that also has padding, under the default box sizing - A fixed-width element inside a narrower container
- A negative margin
- A long unbroken string such as a URL, with no
overflow-wrap
overflow-x: hidden on body hides the symptom and is the reflex fix. It is worth resisting, because it leaves the element overflowing, it can break sticky positioning across the whole page, and on some browsers it makes the page unscrollable in ways that are hard to trace. Find the element instead.
Scrolling only where you mean to
Two properties worth pairing with overflow on any scrolling region.
overscroll-behavior: contain stops a scroll gesture inside a modal or sidebar from continuing onto the page behind it once the inner element reaches its end. That chaining is the reason a scrollable modal moves the page underneath, and one declaration fixes it.
scroll-behavior: smooth animates programmatic scrolling and anchor jumps. Respect the user's preference alongside it:
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
What a scroll container changes
Setting overflow to anything other than visible does more than clip. It creates a block formatting context, and that has consequences you may or may not want.
Floats are contained. A container whose children all float has zero height until something contains them, and overflow: hidden was the standard fix for years. display: flow-root now does the same thing with no other side effects, and is the better choice.
Margins stop collapsing through it. A child's top margin normally escapes its parent and collapses with the parent's own margin. Inside a formatting context it does not, so the parent gains the space instead. This is why adding overflow: hidden sometimes changes vertical spacing on a page in ways that look unrelated.
Sticky descendants break, covered above and in why sticky is not working.
Absolutely positioned descendants can be clipped, if the scroll container is also their positioned ancestor.
Each of those is occasionally the reason someone reached for overflow: hidden in the first place. The point is that you get all four whether you wanted them or not, which is the argument for using the purpose-built property in each case: flow-root to contain floats, clip to clip.
Styling scrollbars
Two approaches, and the standard one is now widely supported:
.scroller {
scrollbar-width: thin;
scrollbar-color: #7c4dff transparent; /* thumb, track */
}
Two properties, works across browsers, and deliberately limited: you get a width keyword and two colours, not arbitrary styling.
The older WebKit pseudo-elements allow much more:
.scroller::-webkit-scrollbar { width: 8px; }
.scroller::-webkit-scrollbar-thumb { background: #7c4dff; border-radius: 4px; }
Use the standard properties first and treat the WebKit ones as an enhancement. Whichever you use, keep the scrollbar visible enough to be found. A scrollbar styled down to invisibility hides the only signal that there is more content, which is a usability problem rather than a clean design.
Scroll snapping
Once you have a scroll container, snapping makes it feel deliberate rather than loose:
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.carousel > * {
scroll-snap-align: start;
flex: 0 0 100%;
}
Two properties: the container declares the axis and the strictness, each child declares where it should land. That is a working carousel with no JavaScript, and it keeps native scrolling, momentum and keyboard support that a hand-built one usually loses.
mandatory always snaps to the nearest point. proximity only snaps when you stop nearby, which is gentler and better for content of varying sizes. Use proximity for a list of cards and mandatory for full-width slides.
Overflow and accessibility
A scrolling region needs to be reachable by keyboard. A <div> with overflow: auto is scrollable with a mouse and, in some browsers, not focusable, so a keyboard user cannot scroll it at all.
<div class="scroller" tabindex="0" role="region" aria-label="Data table">
tabindex="0" makes it focusable so arrow keys work. Firefox and Chrome have improved this in recent versions, and adding it explicitly is still the reliable approach.
The same applies to the horizontally-scrolling table pattern: if the only way to see the rest of the columns is a drag gesture, that content is unreachable for some users.
The horizontally scrolling table
The one pattern worth building properly, because it comes up on every project with data and is usually done badly.
.table-wrap {
overflow-x: auto;
scrollbar-gutter: stable;
}
.table-wrap table { min-width: 40rem; }
Three things make it work. The wrapper scrolls, not the table, so the table keeps its natural layout. A min-width on the table gives it something to overflow, otherwise it shrinks to fit and the columns become unreadable. And scrollbar-gutter: stable stops the layout shifting when the scrollbar appears.
Add the accessibility attributes from the section below and it is complete. Without them, the columns past the right edge are unreachable for anyone not using a mouse, which is the most common failure of this pattern.
Choosing
| Situation | Use |
|---|---|
| Clip a rounded corner | clip |
| Contain floats | flow-root, not hidden |
| A scrollable panel | auto |
| Always show scrollbars for layout stability | scroll, or auto with scrollbar-gutter: stable |
| Truncate a single line of text | hidden, with the other two properties from text truncation |
| Stop a modal scrolling the page behind it | overscroll-behavior: contain |
The general rule: auto for things that should scroll, clip for things that should not, and hidden only when you specifically want a scroll container.
html, body, and which one scrolls
A quirk that causes real confusion: the overflow of html and body is special-cased.
If html has overflow: visible, the browser takes the value from body and applies it to the viewport instead. So body { overflow: hidden } stops the whole page scrolling, which is not what the same declaration would do on any other element.
Once html has its own non-visible overflow, that propagation stops and body behaves like a normal element again.
The practical consequences:
Locking the page behind a modal is usually document.body.style.overflow = "hidden", which works because of this propagation. It also loses the scroll position on some mobile browsers, which is why overscroll-behavior and the inert attribute are better tools where available.
height: 100% on both is a pattern inherited from older layouts and it makes the document a scroll container in a way that can break sticky positioning across the whole page. Modern layouts rarely need it; min-height: 100dvh on a wrapper does the job without the side effect.
Three common mistakes
Reaching for overflow: hidden to clip. It creates a scroll container and breaks sticky descendants. clip does the clipping alone.
Putting overflow-x: hidden on body to kill a horizontal scrollbar. It hides the symptom and creates new ones. Find the overflowing element with the snippet above.
Forgetting the scrollbar takes 15px. A layout that fits exactly will overflow once it starts scrolling, and only on machines with classic scrollbars.
Quick reference
overflow: auto; /* scroll when needed */
overflow: clip; /* clip, no scroll container */
overflow-x: auto; overflow-y: clip; /* note: not honoured, y becomes auto */
scrollbar-gutter: stable; /* reserve the space always */
overscroll-behavior: contain; /* stop scroll chaining */
/* find what is overflowing the page */
document.querySelectorAll("*").forEach((el) => {
if (el.getBoundingClientRect().right > document.documentElement.clientWidth) console.log(el);
});
Want to build scrolling panels and watch the content box shrink? Start with the CSS track.
More from the blog

Flexbox vs Grid: how to choose in ten seconds
Flexbox vs grid: flex sizes items to their content, grid sizes the tracks. Measured, that gave 32px and 229px against three equal 193px columns.
Read more
CSS specificity: why your style is not applying
Specificity is three numbers compared left to right, not one score. Here are the contests that decide it, measured in a browser.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.