# Glass and layers

> Use translucent surfaces where they help orientation, keep text on them legible, and stack layers with a plan instead of z-index 9999.

Module: Color, depth and surfaces · Level: intermediate · 7 min · https://school.agents.org.in/design/surface/glass-and-layers

Scroll the photos under the toolbar. The bar is see-through, so you never lose track of what's behind it.

*[Interactive demo: Drag the sliders, then stack a second glass layer on top and watch the label drown.]*

That is what a translucent material (A see-through, blurred surface (CSS backdrop-filter) such as a toolbar that content scrolls under. Its job is to keep you oriented, not to decorate) 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.

```css
.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 scrim (A dark see-through layer laid over the page behind a dialog, which pushes the page back so the dialog has your full attention): 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:

```css
@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); }
}
```

> **Glass that renders flat.** `backdrop-filter` only blurs what is painted behind the element inside its own group of layers. A parent with `overflow: hidden`, a `transform` or `isolation` can leave it sampling nothing, so the glass shows as a flat tint. Check in Safari and Firefox.

That warning leads straight into the second half of this lesson: layers.

## Who's on top

When things overlap, CSS draws them in order. `z-index` (The CSS number that decides which overlapping thing is drawn on top. It only competes with siblings inside the same stacking context) changes the order. Here is the same screen with three different strategies; each one is real CSS painting live.

*[Interactive demo: Magic numbers bury the toast. A transform traps the modal under the header.]*

`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:

  **Step: 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.

  **Step: 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.

  **Step: Local numbers inside an isolated group.** When a component's own parts must overlap, wrap it in `isolation: isolate` and use 1 and 2 inside. Nothing escapes the wrapper, so local numbers can't collide with global ones.

```ts
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 stacking context (A sealed group of layers. Things inside it are sorted among themselves and the whole group is drawn as one layer, so a z-index of 9999 inside can't beat something outside): 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.)*

**Fix the layers (paste into your agent):**

```text
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.
```

**Check yourself:** A modal has z-index: 9999 but still appears under the sticky header. What's the likely cause?

- [ ] 9999 isn't high enough; use 99999
- [x] A parent of the modal has a transform (or similar), making a stacking context it can't escape
- [ ] The header has position: sticky
- [ ] The modal needs opacity: 1

*Why:* Inside a stacking context, z-index only competes with siblings. Move the modal out (a portal) or remove the property that created the context.

## In the skill

- https://school.agents.org.in/skill/surface/translucent-materials.md
- https://school.agents.org.in/skill/surface/z-index-strategy.md
