Glass and layers
intermediate·7 min·Markdown for agents
What this lesson teaches
Use translucent surfaces where they help orientation, keep text on them legible, and stack layers with a plan instead of z-index 9999.
Scroll the photos under the toolbar. The bar is see-through, so you never lose track of what's behind it.
Scroll the photos under the bar.
That is what a is for. A see-through bar lets content run under the chrome instead of losing a fixed strip of screen to it, and keeps you oriented. Chosen for looks alone, it fails at both.
Glass by hierarchy
Emil Kowalski distils Apple's guidance into a few rules:
- Heavier, more opaque glass for structure (sidebars, tab bars), lighter for the interactive bits you want noticed.
- Never light glass on light glass. Two blurs compound and text legibility collapses. Make one of them solid. (That's the second switch above.)
- Bigger reads thicker. A full sheet gets more blur and a deeper shadow than a small chip.
.toolbar {
background: rgb(255 255 255 / 0.6);
backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgb(255 255 255 / 0.4); /* a lit top edge */
}Modal or parallel
A modal task, one that blocks everything until you answer, pairs its surface with a : a dark see-through layer that pushes the page back. A parallel panel that you work beside stays translucent with no scrim, so your flow isn't broken.
When glass appears, let it materialize: animate the blur and a slight scale together, so it arrives as a material rather than fading in like a ghost.
Text on glass
Flat mid-gray text dies over a backdrop that keeps changing. On glass:
- raise the contrast and step the weight up one,
- add a hair of letter-spacing (about 0.01em),
- put brand color on a solid layer, never on the see-through foreground,
- measure contrast over the worst thing the bar can sit on.
And respect the people who asked for less of it:
@media (prefers-reduced-transparency: reduce) {
.toolbar { background: var(--surface); backdrop-filter: none; }
}
@media (prefers-contrast: more) {
.toolbar { background: var(--surface); border-color: var(--border-strong); }
}That warning leads straight into the second half of this lesson: layers.
Who's on top
When things overlap, CSS draws them in order. changes the order. Here is the same screen with three different strategies; each one is real CSS painting live.
.nav { z-index: 50 }
.modal { z-index: 9999 }
.toast { z-index: 999 }999 felt big. 9999 was bigger. The toast confirming your action now sits under the scrim.
z-index: 9999 is a confession that nobody knows the order. The skill, drawing on Cheng Lou's notes, gives three tools, in order of preference:
No z-index at all
Later elements in the HTML paint over earlier ones. Arrange the markup so source order matches stacking order and most layers need nothing.
One global file for app-wide layers
Nav, menus, tooltips, modals and toasts compete across the whole app, so give them sequential constants in a single file, in order. Adding a layer means inserting one line in the right place.
Local numbers inside an isolated group
When a component's own parts must overlap, wrap it in
isolation: isolateand use 1 and 2 inside. Nothing escapes the wrapper, so local numbers can't collide with global ones.
let d = 1
export const nav = d++, contextMenu = d++, tooltip = d++, modal = d++, toast = d++The sealed box
The "transformed parent" case is the one that confuses everyone. Some properties turn an element into a : a sealed box of layers. Everything inside is sorted among itself, then the whole box is painted as one layer. A child with z-index: 9999 beats its siblings and nothing outside.
What creates the box: a z-index on a positioned element, position: fixed or sticky, opacity below 1, and the quiet ones, transform, filter and will-change: transform.
That's the classic bug. Someone adds a transform for an entrance animation, or will-change "for performance", and a modal inside can no longer rise above the header, while a tooltip that used to escape its card gets clipped. After adding any animation to a container, re-check what's drawn on top.
Clean up stacking in this codebase: - List every z-index. Remove the ones DOM order already handles. - App-wide layers (nav, menu, tooltip, modal, toast) become sequential constants in one file; no magic numbers like 50 or 9999. - Components that overlap internally get isolation: isolate and local values 1–2. - Flag any transform, filter or will-change on a container that holds a modal, menu or tooltip. - Translucent bars: no light glass on light glass; add prefers-reduced-transparency and prefers-contrast fallbacks to a solid surface.
Quick check
A modal has z-index: 9999 but still appears under the sticky header. What's the likely cause?
In the skill
The nodes your agent reads when it does this work.
- Translucent materialsTranslucency is a floating functional layer, not decoration. Material weight encodes hierarchy, never stack light glass on light glass, bigger surfaces read thicker, scrim for modal and none for parallel, vibrant text, scroll-edge masks over dividers, materialize with blur + scale, and solid fallbacks for reduced transparency and more contrast.
surface/translucent-materials - Z-index strategyPrefer DOM order to z-index; where you need it, one global file of sequential layer constants for app-wide layers and local values inside an isolated stacking context. No magic numbers, never mix the two, and know which properties create a stacking context or a clip.
surface/z-index-strategy
Connected lessons
What this lesson builds on and leads to, across every course.