skip to content

Overlays, Fixed Layers, and the Top Layer

Building modals, dropdowns, and tooltips that really do sit above everything: fixed-positioning caveats, escaping clipping ancestors, and the browser top layer that dialog and popover render into. This is where z-index theory becomes a product bug.

part ofCSSoverview, primer and where to startread it →
on this pageshow

questions

6

A dropdown panel positioned with `position: absolute` is cut off at the edge of a card whose CSS sets `overflow: hidden`. What decides whether an ancestor's overflow clips a positioned descendant, and what are the ways out?

level: middleimportance: must knowfreq 56%

answer

  1. clipping follows the containing-block chain
  2. not every ancestor clips a positioned box
  3. who is the panel's containing block?
  4. fixed escapes; the card's relative does not
  5. z-index is not a clipping lever

basics

~20 s

An element with clipped overflow clips only descendants whose containing block is that element or something inside it. A positioned panel whose containing block is higher up escapes the clip — which is why position: fixed, or mounting the panel at the document root, fixes it.

solid answer

~50 s

Clipping follows the containing-block chain, not the DOM tree. A box with `overflow` other than `visible` clips its in-flow content and any descendant whose containing block lies at or below it — but a positioned descendant whose containing block is an ancestor *above* the clipping box is not clipped. So the dropdown is cut off because the card is `position: relative` (or otherwise the panel's containing block), which puts the panel inside the clipped area. Options: make the panel `position: fixed`, so its containing block is the viewport and the card's `overflow` no longer applies — assuming no transformed or contained ancestor in between; remove the clip from the card, usually by replacing `overflow: hidden` with something scoped to the element that actually needs it; or mount the panel in a root-level overlay container. Raising `z-index` never helps: clipping happens before stacking is considered.

code

css · 10 lines
css
.card {
  position: relative;
  overflow: hidden;
}

.card .dropdown {
  position: absolute;
  top: 100%;
  left: 0;
}

go deeper

for a junior

Recognise the symptom: a dropdown cut off at a container edge usually means an ancestor has overflow set to something other than visible. Check the ancestors before touching the panel's own rules.

for a middle

Explain the containing-block rule that decides clipping, and show why the same card being both position: relative and overflow: hidden is what traps the panel.

for a senior

Weigh the escapes out loud — fixed positioning versus a root overlay container versus removing the clip — and name the repositioning obligation each one creates in a scrolling layout.

for a principal

Set the convention: overlays mount in one known place, overflow: hidden is never applied to generic layout containers as a default, and shared components document what they clip.

## The rule, stated precisely When a box has `overflow` other than `visible` — `hidden`, `auto`, `scroll`, or `clip` — its content is clipped to its **padding box**. The subtlety interviewers probe is *which* descendants that clipping covers. The answer is not "all of them": clipping applies to the box's in-flow content and to descendants whose **containing block** is the clipping box itself or a descendant of it. A positioned descendant whose containing block sits **above** the clipping box escapes the clip entirely. That gives a mechanical diagnostic. Take the dropdown panel and ask: what is its containing block? - `position: absolute` → the padding box of the nearest **positioned** ancestor (or one that establishes a containing block via transform, filter, or containment). - `position: fixed` → the viewport, unless some ancestor captures it via transform, filter, perspective, `will-change`, `contain`, or `container-type`. If that containing block is inside the clipping card, the panel is clipped. If it is outside, the panel is drawn over the card's edge, overflow declaration and all. ## Why the card version fails ```css .card { position: relative; /* becomes the panel's containing block */ overflow: hidden; /* and clips it */ } .card .dropdown { position: absolute; top: 100%; } ``` Both declarations are on the same element, so the panel's containing block is the very box doing the clipping. The panel is clipped at the card's padding edge. Note the classic accidental version: `overflow: hidden` was added to the card for an unrelated reason — clipping a rounded image corner, containing floats, or hiding a decorative element — and nobody connected it to the dropdown that appears months later. ## The escapes, and their costs **Switch the panel to `position: fixed`.** Its containing block becomes the viewport, which is above the card, so the card's overflow no longer applies. The cost is that fixed boxes do not move with the card: if the card scrolls, you must reposition the panel yourself, or use anchor positioning where supported. This also fails if any ancestor establishes a containing block for fixed boxes, which converts one bug into another. **Move the panel out of the clipped subtree.** Mount overlays in a container at the end of `<body>`. Nothing above it clips, nothing above it captures, and painting order becomes predictable. This is the general answer for a design system and the reason the portal pattern exists. Cost: the panel loses its natural position next to the trigger and must be positioned deliberately. **Remove the clip.** Often the right fix. `overflow: hidden` is a blunt instrument; if it was added to clip a decorative image, put the clip on the image wrapper instead. If it was added to contain floats, `display: flow-root` does that without clipping. If it was added because a child overflows horizontally, fixing the child's sizing is better than hiding the symptom. **Do not** reach for `z-index`. Clipping is a rasterization boundary applied before any stacking comparison: a clipped box at `z-index: 99999` is still clipped, it is simply clipped and on top. ## The scroll-container variant The same rule explains a second familiar bug. Inside a scrollable list (`overflow: auto`), an absolutely positioned row menu whose containing block is a row inside the list gets clipped at the list's edges. `position: fixed` escapes the clip — and now the menu does not scroll with its row, so it hangs in place while the list moves under it. Whichever escape you choose, you have traded clipping for a positioning obligation, and the honest answer in an interview names that trade rather than presenting one line of CSS as a cure. ## Sanity check in DevTools Select the panel, walk up its ancestors, and note the first ancestor with non-`visible` overflow and the first one that is its containing block. If the containing block is at or below the clipper, you have the bug; the order of those two ancestors is the whole story.

  • If the card kept `overflow: hidden` but dropped `position: relative`, would the absolutely positioned dropdown still be clipped?
    Not by that card. Its containing block would resolve to some positioned ancestor further up — or the initial containing block — which is above the clipper, so the panel is not clipped. It is a real technique, but a fragile one: anyone who later adds `position: relative` to the card silently reintroduces the bug.
  • What breaks when you solve this by switching the panel to `position: fixed`?
    The panel stops moving with its trigger. Scrolling the page or an inner container leaves it stranded, so you owe it repositioning logic or CSS anchor positioning. It also breaks again if any ancestor has a transform, filter, or containment, since that ancestor becomes the containing block for fixed boxes.
  • Why is `display: flow-root` sometimes the better replacement for `overflow: hidden` on a container?
    When `overflow: hidden` was added only to make a container enclose floated children, `display: flow-root` achieves the same by establishing a block formatting context, without clipping anything. Overlays inside keep working, and nothing else about the box changes.

saying these in an interview costs you the question

  • Says overflow: hidden clips every descendant unconditionally
  • Tries a larger z-index to escape a clip
  • Assumes position: fixed always escapes any ancestor
  • Adds overflow: visible to the clipped panel itself
  • Never checks which element is the containing block

context

open as a page

A modal styled `position: fixed; inset: 0` covers only part of the page and scrolls away with the content instead of staying pinned to the viewport. Its ancestor card declares `transform: translateZ(0)`. Why does the transform break the modal?

level: middleimportance: must knowfreq 64%

basics

~20 s

A transformed ancestor becomes the containing block for fixed-position descendants, so the modal is laid out against that card's padding box rather than the viewport. Move the overlay out of the transformed subtree, or drop the transform.

open as a page

What does it mean for an element to render in the browser's top layer — as a `<dialog>` opened with `showModal()` does — and how does that change z-index, clipping, and its containing block?

level: middleimportance: should knowfreq 46%

basics

~20 s

The top layer is a viewport-sized painting layer above the whole document. An element promoted into it paints over all page content whatever their z-index values, is not clipped by ancestor overflow, and is positioned against the viewport rather than a transformed ancestor.

open as a page

In a large application, modals, dropdowns, and toasts keep escalating their z-index values to fight each other, and new overlays regularly appear behind existing chrome. How would you restructure the overlay layer so this stops recurring?

level: principalimportance: should knowfreq 34%

basics

~20 s

Stop letting overlays compete where they are authored. Mount them in one root-level container or the browser top layer, define a small ordered scale of named layers, and forbid arbitrary z-index values in components — the escalation is a structural problem, not a numbering one.

open as a page

In CSS, what does the `::backdrop` pseudo-element style, and which elements have one?

level: juniorimportance: nice to knowfreq 30%

basics

~20 s

::backdrop is a browser-generated box painted immediately behind an element that is rendering in the top layer — a modal <dialog>, a shown popover, or a fullscreen element — covering the whole viewport. It is where you style the dimming scrim.

open as a page

What problem does CSS anchor positioning (`anchor-name`, `position-anchor`, the `anchor()` function) solve for tooltips and popovers, and how would you ship it while support is uneven?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Anchor positioning lets an absolutely or fixed positioned overlay resolve its offsets from another element's box instead of its containing block, so a tooltip stays attached to its trigger without JavaScript measurement — including when the overlay is in the top layer.

open as a page