Overflow and Visibility
Overflow: What Happens When Content Doesn't Fit
Every element is a box. When the content inside exceeds the box's dimensions, the browser has to make a call: let it overflow visually, clip it silently, or add scrollbars. The overflow property controls this -- and it has subtle interactions that will break your layouts if you don't understand the rules.
And then there's hiding elements. The three methods -- display: none, visibility: hidden, and opacity: 0 -- each have fundamentally different effects on layout, accessibility, and performance. Pick the wrong one and you're either hiding content from screen readers when you shouldn't be, or leaving invisible elements that still eat clicks.
Think of overflow as a window frame. The element is the frame, the content is the view through it. visible means the frame doesn't exist — content extends beyond freely. hidden means the frame clips the view with no way to see more. scroll adds a scroll mechanism (scrollbars always present). auto adds scrollbars only when content exceeds the frame. clip is like hidden but even stricter — it prevents programmatic scrolling too.
overflow-x and overflow-y Interaction
You can set overflow independently for each axis. Sounds simple, right? But they interact in genuinely surprising ways:
.box {
overflow-x: hidden;
overflow-y: visible; /* You might expect content to overflow vertically */
}
/* ACTUAL RESULT: overflow-y becomes 'auto', not 'visible' */
The rule: If one axis is set to hidden, scroll, or auto, and the other is set to visible, the visible value is converted to auto. You cannot have hidden on one axis and visible on the other.
/* What you want: clip horizontal overflow, allow vertical to show */
/* What you get: clip horizontal, auto (scroll) vertical */
/* Workaround: use clip instead of hidden */
.box {
overflow-x: clip;
overflow-y: visible; /* Now this actually works */
}
overflow: hidden creates a Block Formatting Context (BFC). This means it also prevents margin collapse, contains floats, and changes how absolutely positioned descendants behave. Switching from overflow: visible to overflow: hidden can cause unintended layout changes beyond just clipping content.
overflow Values Compared
/* visible (default) — content overflows freely */
.a { overflow: visible; }
/* hidden — content clipped, no scrollbar, but scrollable via JS */
.b { overflow: hidden; }
/* element.scrollTo(0, 100) still works */
/* clip — content clipped, NOT scrollable even via JS */
.c { overflow: clip; }
/* element.scrollTo() has no effect */
/* Does NOT create a BFC (unlike hidden) */
/* scroll — scrollbars always present (even if content fits) */
.d { overflow: scroll; }
/* auto — scrollbars appear only when content overflows */
.e { overflow: auto; }
text-overflow for Truncation
.truncate {
white-space: nowrap; /* Prevent text wrapping */
overflow: hidden; /* Clip the overflow */
text-overflow: ellipsis; /* Show ... at the clip point */
}
/* Multi-line truncation */
.truncate-multiline {
display: -webkit-box;
-webkit-line-clamp: 3; /* Show 3 lines */
-webkit-box-orient: vertical;
overflow: hidden;
}
The Three Ways to Hide Elements
Not all hiding is created equal. Let's break them down.
display: none
.hidden { display: none; }
- Layout: Removed entirely — no space reserved
- Accessibility: Invisible to screen readers
- Interaction: Cannot receive focus or clicks
- Animation: Cannot transition to/from
display: none(but see@starting-style) - Use when: The element should not exist for anyone
visibility: hidden
.invisible { visibility: hidden; }
- Layout: Space is preserved — a blank gap remains
- Accessibility: Invisible to screen readers
- Interaction: Cannot receive clicks, but tab order may be affected
- Animation: Can transition
visibility(snaps between hidden/visible) - Children: Children can be
visibility: visibleand appear normally - Use when: You want to preserve layout space (e.g., preventing layout shift)
opacity: 0
.transparent { opacity: 0; }
- Layout: Space is preserved
- Accessibility: Still accessible to screen readers
- Interaction: Still receives clicks and focus
- Animation: Can smoothly transition opacity (GPU-composited)
- Use when: You're animating an element in/out
The Accessible Hide Pattern
This one matters a lot. For content that should be available to screen readers but invisible on screen:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
This pattern is used by every major design system (Tailwind, Chakra, Radix). display: none won't work here because it removes the element from the accessibility tree.
Production Scenario: Scroll Container Gotchas
/* Problem: Horizontal scrolling on the whole page */
.hero-image {
width: 120vw; /* Oops — extends beyond viewport */
}
/* The page now has a horizontal scrollbar */
/* Fix 1: Clip the specific container */
.hero { overflow-x: clip; }
/* Fix 2: Clip at the body level */
body { overflow-x: clip; }
/* Using 'hidden' works too, but 'clip' doesn't create a BFC */
/* Problem: Sticky header doesn't work inside overflow container */
.container {
overflow: auto; /* This breaks position: sticky for descendants */
height: 400px;
}
.sticky-header {
position: sticky;
top: 0;
/* Sticks relative to the scroll container, not the viewport */
/* If .container is the scroller, sticky works within it */
/* But if you wanted viewport-level sticky, overflow: auto blocks it */
}
| What developers do | What they should do |
|---|---|
| Setting overflow-x: hidden and overflow-y: visible expecting both to work independently The CSS spec converts visible to auto when the other axis is hidden, scroll, or auto | Use overflow-x: clip with overflow-y: visible — clip doesn't force the other axis to auto |
| Using display: none for elements that screen readers should announce display: none removes elements from the accessibility tree entirely | Use the sr-only pattern (absolute positioning, 1px clipping) |
| Using opacity: 0 to hide interactive elements without disabling interaction opacity: 0 elements are still fully interactive — they receive clicks and focus | Add pointer-events: none if the hidden element shouldn't be clickable |
| Expecting position: sticky to work when a parent has overflow: hidden or auto overflow: hidden/auto/scroll on an ancestor makes that ancestor the sticky boundary | sticky works relative to its scroll container. If overflow creates a scroll container, sticky sticks within that, not the viewport. |
- 1overflow: hidden on one axis converts overflow: visible on the other axis to auto
- 2overflow: clip is like hidden but doesn't create a BFC and prevents programmatic scrolling
- 3display: none removes from layout AND accessibility tree — use sr-only for accessible hiding
- 4opacity: 0 preserves layout, accessibility, AND interactivity — add pointer-events: none to disable clicks
- 5overflow: hidden/auto/scroll on ancestors creates a scroll container that captures position: sticky