Depth & z-axis model
themes specs/themes/depth.kmd
The cross-surface z-axis model. Defines depth as ONE abstract scale with two projections: 2D (rendered as shadow + tonal lift via `elevation.kmd`) and spatial (rendered as literal centimetres via `xr-preview.kmd`). Owns the stacking / z-order contract and the depth→projection mapping so a surface picks a depth level once and renders coherently on web, desktop, mobile, TV and XR. Material parity: generalizes M3 elevation into a surface-agnostic axis.
When this spec applies
Primary triggers
- Place any Koder surface on the z-axis
All triggers
- Decide which depth level a new surface or component sits at
- Reason about stacking / z-order between overlapping surfaces
- Map a 2D elevation to its XR centimetre offset (or back)
- Add a surface that must render on both 2D and XR
Specification body
Spec — Depth & z-axis model
Facet Visual do Koder Design. Material parity: generalizes https://m3.material.io/styles/elevation from a 2D shadow scale into a surface-agnostic depth axis.
Koder Design has one depth axis. A surface picks a depth level once; each surface technology then projects that level:
- 2D (web/desktop/mobile/TV) → shadow + tonal lift — see
elevation.kmd. - Spatial (XR) → literal forward offset in centimetres — see
xr-preview.kmd.
This spec owns the axis and its projection contract;
elevation.kmd owns the 2D shadow recipes; xr-preview.kmd owns the
spatial placement. Pick depth here, render there.
R1 — The depth scale
6 levels (0–5), shared with elevation by construction:
| Depth | Meaning | 2D projection | XR projection |
|---|---|---|---|
| 0 | On the page plane | no shadow | flush with volume back-plane |
| 1 | Just lifted | --shadow-1 + tonal | ~0.5 cm forward |
| 2 | Raised | --shadow-2 | ~1.0 cm |
| 3 | Floating | --shadow-3 | ~1.5 cm |
| 4 | Hovering above content | --shadow-4 | ~2.0 cm |
| 5 | Top of the stack (modal) | --shadow-5 | ~2.5 cm |
Canonical mapping: 1 depth level ≈ 0.5 cm of forward offset in XR
(xr-preview.kmd § R3). One token, --kds-depth-level, carries the
integer; renderers read it.
R2 — Depth is relative to the nearest stacking context
A depth level is local to its container, not a global z-index. A level-3 menu inside a level-5 dialog is still "above" its dialog parent — depth composes within a stacking context, it doesn't reset to absolute screen z.
Rule (from elevation.kmd § R5): a child's depth must be ≥ its
parent's. A surface that needs to escape its parent's stacking
context (e.g. a tooltip over a clipped scroll area) is portaled to
a higher context, then takes its depth there.
R3 — Canonical z-order (stacking bands)
Overlapping surface families occupy fixed bands so a popover never loses to a sticky header by accident:
| Band | Families | Depth |
|---|---|---|
| Base | page background, content | 0 |
| Raised | cards, sticky headers, app bars | 1–2 |
| Floating | FAB, contextual toolbars | 3 |
| Overlay | menus, dropdowns, tooltips, snackbars | 3–4 |
| Modal | dialogs, sheets, drawers + their scrim | 5 |
| System | toasts/critical alerts, command palette | 5 (above scrim) |
CSS z-index is allocated in matching ranges via tokens
(--kds-z-raised, --kds-z-overlay, --kds-z-modal, --kds-z-system)
— never hand-pick a z-index integer.
R4 — Parallax & perceived depth (2D)
On flat surfaces, depth can also be implied by motion (pointer/gyro parallax, depth-on-scroll). That is a perceptual cue, not a real layer:
- Parallax magnitude SHOULD scale with depth level (deeper = moves less, like distant scenery).
- Perceived-depth motion lives in
themes/motion/spatial.kmdand is always gated byprefers-reduced-motion(freezes to the static layout). - Parallax MUST NOT reorder the real stacking bands (R3).
R5 — Forbidden patterns
- ❌ Raw
z-indexintegers in widget code (use band tokens, R3). - ❌ Child surface at lower depth than its parent (R2).
- ❌ Re-deriving cm offsets per component instead of from the level (R1 owns the 0.5 cm mapping).
- ❌ Using depth purely decoratively — depth implies the lighting
model casts a shadow (
lighting.kmd); flat decoration is depth 0. - ❌ Parallax that changes which surface is "on top" (R4).
R6 — Accessibility
- Depth is never the only signal (mirror
elevation.kmd § R7): pair with border, position, or tonal change. forced-colors: active→ projections collapse; depth shown by border weight (elevation.kmd § R6).- Reduced-motion freezes all perceived-depth motion (R4); the static stacking order (R3) is unaffected.
T1-T3 — Tests
T1 — Mapping: for every depth level, the 2D projection resolves the
matching --shadow-N and the documented XR cm offset (level × 0.5).
T2 — No raw z-index: koder-spec-audit depth --zindex finds zero
hard-coded z-index integers outside the band-token definitions.
T3 — Parent ≥ child: render-tree audit finds no surface whose depth is below its stacking-context parent.
Cross-link
themes/elevation.kmd— 2D projection (shadow + tonal)themes/lighting.kmd— the rig that makes depth cast shadowdevelop/xr-preview.kmd— spatial projection (centimetres)themes/motion/spatial.kmd— perceived-depth motion (parallax)app-layout/safe-area.kmd— XR volume bounds the z-axis lives in
References
specs/themes/elevation.kmdspecs/themes/lighting.kmdspecs/develop/xr-preview.kmdspecs/app-layout/safe-area.kmd