Overflow Clip and Hidden
Per-axis overflow semantics — pure paint clipping with clip vs scroll containers with hidden, the forcing rule, and the grid paint-through bug class
The Problem
AI agents treat overflow: hidden as the universal fix for unwanted overflow. It is not — hidden does more than clip paint, and the extra behavior produces a recognizable class of bugs. Two real review findings turned entirely on overflow semantics:
In an app-shell grid (
grid-template-rows: auto 1fr auto), oversized content in the body row painted through the sibling footer row.min-height: 0kept the track geometry stable, but the defaultoverflow: visiblelet the content overlay the next row.The naive fix —
overflow: hiddenon the body — stopped the paint-through but sheared the inline-axis box-shadows of full-width children flat at the padding edge.
Both failures come from two under-known rules. First, hidden creates a scroll container, with scroll side effects you did not ask for. Second, the two overflow axes are not independent: visible cannot coexist with hidden, scroll, or auto on the other axis — it is silently forced to auto. The correct fix for the panel was neither hidden nor a wrapper element: overflow-x: visible; overflow-y: clip.
The Solution
Choose the overflow value by what you actually need:
A real scroll area (user scrolling,
scrollTo, the single-line ellipsis recipe) — useauto,scroll, orhidden.Paint clipping only — use
clip. It creates no scroll container, does not force a new formatting context, lets the browser skip scroll machinery entirely, and acceptsoverflow-clip-margin.Clipping on one axis while decorations (shadows, outlines) breathe on the other — use
overflow-x: visible; overflow-y: clip(or the transposed pair).visible+clipis the only mixed pair the forcing rule leaves untouched.
clip vs hidden
Both values clip paint at the box edge. Everything else differs:
| Behavior | overflow: hidden | overflow: clip |
|---|---|---|
| Clips paint | Yes, at the padding box | Yes, at the overflow clip edge (adjustable via overflow-clip-margin) |
| Creates a scroll container | Yes — programmatically scrollable (scrollTop, scrollIntoView) | No — the box cannot scroll at all |
| Scrolls on focus / fragment navigation | Yes, content shifts silently with no scrollbar to recover it | No |
| Participates in scroll anchoring | Yes | No |
| Establishes a new formatting context | Yes | No |
Pairs with visible on the other axis | No — forces it to auto | Yes |
/* A scroll container you cannot see or operate */
.card {
overflow: hidden;
}
/* Pure paint clipping */
.card {
overflow: clip;
}The scroll-container difference is not theoretical. A hidden box scrolls whenever the browser decides something inside must be revealed: keyboard focus moving to an off-screen descendant, location.hash navigation, scrollIntoView from a library. The content shifts, there is no scrollbar to shift it back, and the layout looks corrupted. A clip box cannot be scrolled by anything.
Note
Focusable content hidden behind any clipping is an accessibility smell — keyboard users land on an element they cannot see. The demo only proves the scrollability difference; the real fix for interactive content is to not clip it.
The Per-Axis Forcing Rule
overflow is a shorthand for overflow-x and overflow-y, and the two axes are not independent. When one axis is hidden, scroll, or auto, a visible on the other axis computes to auto, and a clip computes to hidden. You silently get a scroll container on the axis you wanted left alone.
| Declared | Computed | Effect |
|---|---|---|
overflow-y: hidden (x left as visible) | overflow-x: auto; overflow-y: hidden | Silent scroll container on the x axis |
overflow-x: hidden (y left as visible) | overflow-x: hidden; overflow-y: auto | Silent scroll container on the y axis |
overflow-x: clip; overflow-y: auto | overflow-x: hidden; overflow-y: auto | The clip is upgraded to a scroll-container value |
overflow-x: visible; overflow-y: clip | Unchanged | The one legal mixed pair |
visible + clip is the only combination that survives as written — and therefore the only way to say "clip block-axis overflow, let inline-axis decorations breathe."
/* What you wrote */
.track {
overflow-y: hidden;
}
/* What you got */
.track {
overflow-x: auto; /* forced — a scroll container you never declared */
overflow-y: hidden;
}
/* The only way to clip one axis and keep the other truly visible */
.track {
overflow-x: visible;
overflow-y: clip;
}The Grid Paint-Through Bug
Grid and flex tracks have two separate failure layers when content is oversized: geometry and paint. min-height: 0 (or min-width: 0) fixes geometry — the track stops growing to fit its content. It does nothing about paint: with the default overflow: visible, the item's content keeps painting past the track edge, overlaying the footer row below and escaping the panel entirely. Grid items paint atomically in document order, so an opaque background on the footer masks its own strip — but that only hides the overlap; the overflow still runs past the panel border below it. Nothing contains the content until the track's item clips it.
.app-panel {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100dvh;
}
.app-panel__body {
/* Step 1: geometry — let the track shrink below its content */
min-height: 0;
/* Step 2: paint — stop content from overlaying the footer */
overflow-y: clip;
}Both steps are required. min-height: 0 alone leaves the paint-through; overflow-y: clip alone leaves the track oversized. And the clip must be the per-axis form: the shorthand overflow: clip (or overflow: hidden) also clips the inline axis, which shears shadows and outlines — the next section. The min-width: 0 / min-height: 0 reset itself is covered in Flexbox Patterns.
Shadow Shearing and the Per-Axis Fix
Clipping both axes cuts more than layout overflow — it cuts decorations. Full-width children inside an overflow: hidden container have their inline-axis box-shadows sheared flat at the container's padding edge, because the shadow paints outside the child's border box and the container clips everything beyond its own. The per-axis pair keeps the block-axis clip and releases the inline axis, so shadows and outlines render complete. Shadow construction itself is covered in Smooth Shadow Transitions.
/* NG: shears the shadows flat at the inline edges */
.card-stack {
overflow: hidden;
}
/* OK: clips the block axis, lets inline-axis shadows breathe */
.card-stack {
overflow-x: visible;
overflow-y: clip;
}Both stacks clip the third card at the bottom — the block-axis clip works identically. Only the inline axis differs: the left stack cuts every shadow flat at the dashed border, while the right stack lets them bleed onto the background.
overflow-clip-margin
clip has one more capability hidden lacks: the clip edge can be pushed outward. overflow-clip-margin sets how far outside the padding box content may paint before being clipped — enough room for focus rings, glows, and small decorative bleeds to survive while runaway content is still contained.
.glow-frame {
overflow: clip;
overflow-clip-margin: 16px; /* clip 16px outside the padding box */
}Browser support
overflow: clip itself is supported everywhere (Baseline 2022). overflow-clip-margin is supported in Chromium and Firefox but has not shipped in Safari — treat it as progressive enhancement: the clip still happens, only the extra margin is lost. Current implementations also apply it only when both axes compute to clip; it does not combine with the visible + clip pair.
Quick Reference
| Scenario | Technique |
|---|---|
| Real scroll area (user or programmatic scrolling) | overflow: auto, or hidden + script-driven scrolling |
| Single-line ellipsis | overflow: hidden; text-overflow: ellipsis; white-space: nowrap |
| Decorative clipping — no scrolling ever | overflow: clip |
| Clip block axis, keep inline-axis shadows/outlines | overflow-x: visible; overflow-y: clip |
| Grid/flex track content painting over siblings | min-height: 0 (geometry) + overflow-y: clip (paint) |
| Let glows/focus rings survive a clip | overflow: clip; overflow-clip-margin: 8px |
Common AI Mistakes
Defaulting to
overflow: hiddenfor any unwanted overflow.hiddencreates a scroll container. Fragment navigation,scrollIntoView, and keyboard focus can silently scroll it, shifting the layout with no scrollbar to recover it. Useclipwhen you only need to cut paint.Assuming
overflow-y: hiddenleaves the other axis alone. The forcing rule computes the untouchedvisibleaxis toauto— a horizontal scroll container you never declared.Stopping at
min-height: 0when grid or flex content overflows a track. The min-size reset fixes track geometry only. Content still paints through the following rows until the item clips it withoverflow-y: clip.Fixing paint-through with
overflow: hiddenand shipping sheared shadows. The shorthand clips both axes, cutting inline-axis box-shadows and outlines flat at the padding edge. Useoverflow-x: visible; overflow-y: clip.Expecting
overflow-clip-marginto work withhidden. It applies only toclip, and current implementations honor it only when both axes areclip.Expecting a
clipbox to scroll.clipboxes are not scroll containers:scrollTopassignments are ignored andscrollIntoViewcannot reveal clipped content. If anything must ever scroll the box, useautoorhidden.
When to Use
Use hidden when
You need scroll-container semantics without visible scrollbars: the single-line ellipsis recipe (overflow: hidden; text-overflow: ellipsis; white-space: nowrap), script-driven scrollers built on scrollTo, or measuring scrollable content. Accept the side effect that focus and fragment navigation can scroll the box.
Use clip when
You only need to cut paint and nothing should ever scroll: containing decorative bleed, a letterbox stage, print layouts where scroll containers are meaningless, or oversized background flourishes. clip is the cheaper and side-effect-free default for all decorative clipping.
Use the visible + clip pair when
One axis must clip while the other must stay truly visible: app-shell tracks whose content paints through sibling rows, and any container whose children carry inline-axis shadows, glows, or focus outlines that must render complete. visible + clip is the only per-axis combination the forcing rule leaves intact.