Skip to content

Overflow and Visibility

beginner10 min read

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.

Mental Model

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 */
}
Common Trap

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: visible and 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.

Execution Trace
display: none
Element removed from render tree
No layout space, no paint, no accessibility
visibility: hidden
Element in render tree but not painted
Layout space preserved, invisible, not accessible
opacity: 0
Element painted but fully transparent
Layout space preserved, accessible, still interactive
sr-only pattern
Visually hidden, 1px positioned offscreen
Invisible on screen, fully accessible to screen readers
content-visibility: hidden
Skip rendering until needed
Like display: none for rendering, but preserves state

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 doWhat 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.
Quiz
An element has opacity: 0 and a click handler. What happens when a user clicks where the element is?
Quiz
What is the actual computed value of overflow-y when you set overflow-x: hidden and overflow-y: visible?
Key Rules
  1. 1overflow: hidden on one axis converts overflow: visible on the other axis to auto
  2. 2overflow: clip is like hidden but doesn't create a BFC and prevents programmatic scrolling
  3. 3display: none removes from layout AND accessibility tree — use sr-only for accessible hiding
  4. 4opacity: 0 preserves layout, accessibility, AND interactivity — add pointer-events: none to disable clicks
  5. 5overflow: hidden/auto/scroll on ancestors creates a scroll container that captures position: sticky