opsinjs
FoundationsAcross the system

Layering and elevation

The fixed stacking order every overlay, sheet, popover and toast belongs to, and the health rule that nothing may ever cover the value it is talking about.

Overview

Layering is the question of what sits in front of what, and it is almost always solved badly by accident: a z-index: 9999 here, a position: relative there, and eventually a bottom sheet that opens behind the tab bar on one screen and in front of it on another.

opsinjs answers it once, with a small ordered set of named layers. A component never chooses a number; it declares which layer it belongs to, and the layer decides. The set is deliberately short, because every additional layer is a new pair of things whose relative order somebody will get wrong.

This is not the same thing as Materials, and the two are constantly confused. Materials decides what a surface is made of. That is a question of how translucent it is, how blurred, and how it reads over a busy backdrop. Layering decides where it sits in the stack. A dialog and a popover can be built from the same material rung and still belong to different layers.

How it works

Layers are ordinal and named for their job, not for a number:

LayerWhat lives hereHealth consequence
basePage content, cards, lists, chartsThe reading itself lives here
stickySection headers, sticky column headers, in-page toolbarsMust never cover a value it scrolled past
raisedDropdown shadows, drag previews, reorder placeholdersTransient; nothing may be read from it
overlayPopovers, menus, tooltips, comboboxesAnchored, dismissible, non-modal
scrimThe dimming behind a modal surfaceEstablishes modality; see below
modalDialogs and sheetsOwns focus; blocks everything under it
notifyToasts and transient confirmationsBudgeted. See the alarm rules
criticalEscalations that must never be occludedReserved; see the rule below

Two things make this hold rather than drift.

One stacking context per layer. Every layer is rendered into a portal at the document root, so a component's position in the React tree has no effect on its position in the stack. This is why a popover inside a scrolling card is not clipped by it, and why the layer order is the same on every screen.

The scrim belongs to the layer above it. A scrim is not a background; it is the lower half of a modal surface. It is rung 5 of the material ladder, and it is the component's responsibility to render it, so a modal cannot exist without one and a scrim cannot be left behind when a modal closes.

Elevation is the shadow, and it is a consequence of the layer, not an independent choice. Each layer maps to a fixed material rung, and a component may not raise its own shadow to look more important. Importance is carried by clinical status, which is a colour and a word, not a drop shadow.

Using it

Pick the lowest layer that works. Most things belong to base. If a surface does not need to escape its parent's overflow and does not need to sit above other content, it is not an overlay and portalling it only makes focus order harder to reason about.

The health rule: never occlude the value. A surface that explains, qualifies or escalates a reading must not cover that reading. A bottom sheet opened from a result must leave the result visible above it, or restate it inside itself. A toast confirming that a log entry saved must not sit over the number that was just entered.

Do

A sheet that explains a reference range opens to a detent that keeps the reading and its status visible above the sheet edge. The reader can compare the explanation to the thing being explained.

Don’t

A full-height sheet that covers the reading. The reader now has to hold the number in working memory while reading about it. Such a reader is often worried, which is the worst possible condition for that. Use a detent, or restate the value in the sheet header.

critical is reserved and is not a general escape hatch. It exists for one case: an escalation that the product's clinical governance has decided must be seen, which must therefore not be coverable by a toast, a tooltip or a half-dismissed sheet. Using it for anything else re-creates the z-index: 9999 problem with better naming. What may go there is decided on Emergency and escalation, not here.

Do not nest modality. A dialog opened from a dialog is a sign the first dialog was doing too much. Where a second step is genuinely needed, replace the content of the existing modal surface and keep one focus trap; two traps is how a screen-reader user ends up somewhere they cannot leave.

Tokens

Layer order and the material rung each layer resolves to are generated from tokens/space.json and tokens/material.json by scripts/build-tokens.mts. The proposed custom-property shape is --opsin-layer-<name>, one per row of the table above, each paired with the material rung it resolves to. The authoritative list is the generated one, never this page.

This table has not been generated. It is produced by scripts/build-tokens.mts. Run pnpm run generate and reload.

Nothing on this site types a measured number by hand, so an ungenerated table shows this rather than an example.

Accessibility impact

Stacking order and focus order are different orderings of the same interface, and where they disagree, people using a keyboard or a screen reader get the wrong one. Three consequences follow, and all three are checkable:

  • Modality must be real, not painted. A surface on modal traps focus, marks the content beneath it inert, and returns focus to the element that opened it. A dimmed background with no focus trap is the single most common overlay failure and it is invisible to sighted mouse users.
  • notify must not steal focus. Toasts are announced through a live region and are reachable by a documented keyboard route; they never move focus, and they never carry the only copy of information a person needs. This is also the rule that stops a toast becoming an alarm channel. See Alarm fatigue.
  • Nothing on any layer may sit under the safe area. Notches, home indicators and on-screen keyboards remove usable space; a dismiss control that ends up beneath one is not merely awkward, it is unreachable. Safe-area handling is specified on Layout and measured against the 44pt floor on Density and touch.
  • Materials → Choosing a layer says which ladder rung each of these layers is made from, and the nesting rules between them.
  • Layout covers safe areas, keyboard insets and the two canonical health screen shells the layers sit inside.
  • Interaction states is where the focus-visible contract lives. Focus is a state and a layer problem at once.

On this page