Layers
Depth in Noctis comes in two forms — shades within one surface theme, and scopes that re-generate a whole subtree at a shifted base. Telling them apart is the difference between a clean hierarchy and a muddy one — every overlay reads as a distinct plane, and nothing collapses into the canvas behind it.
Two kinds of depth
A shade is a tier within a single surface theme — the neutral ramp you pick from for the canvas, cards, hovers, and wells. A scope is a whole subtree re-generated: set data-elevation and the engine re-runs the entire generator at a base shifted by that scope's ELEVATION_STEP, injecting the resulting scope canvas as a verbatim bg-1 so surfaces, borders, text, and controls all re-derive together from it — not extra tiers bolted onto the root ramp, but a fresh theme. Shades move you up and down one ramp; a scope swaps the ramp underneath you.
Shades — the ramp within one scope
A neutral ramp ordered low to high: a recessed well sits below the canvas base, then it climbs to the highest surface. Pick the shade by role, not by eye — the same name lands at a different luminance in every scope, but always at the same rung of the ramp.
The elevation grid
Each scope re-runs the whole ramp off its shifted base. Read down a column to see one shade at every scope; read across a row to see a single scope's ramp. Raised scopes lift toward white in either mode — the magnitude is unsigned, so elevated and menu rise above the canvas in dark and light alike, with menu the highest and reaching near-white in light — while the sunken scope recedes; the well of the sunken row is the darkest cell of all, the near-black a doubly-recessed surface lands on.
Which scope each surface uses
Overlays establish their own scope and paint its base (bg-background), so a control inside an overlay sits on that re-derived base and separates cleanly. Cards and wells are shades on whatever scope they already live in. The rows below are real component defaults — grep the scope value in any component source to confirm it.
| Surface | Scope | Paints with |
|---|---|---|
| Page canvas / app background | root | bg-background |
| Cards, content panels | root | bg-surface |
| Content wells, code blocks, insets, scrollbar track | root | bg-sunken |
| Sidebar, Rail (page chrome) | root | bg-background |
| Dialog, Sheet, Select | elevation="elevated" | bg-background |
| Dropdown (Menu), Context menu, Command palette (Search dialog), Popover, Tooltip | elevation="menu" | bg-background |
| Buttons, menu items | any | bg-control |
| Inputs, text fields | any | bg-field |
bg-control and bg-field are not the same rung. bg-control is a control-tinted fill (engine control-2); bg-field is a lifted surface (engine bg-3, the surface tier), so an input reads as a recessed-into-surface slot while a button reads as a raised control — different positions on the ramp, never interchangeable.
Building on the levels
The scope lives in the Surface primitive, not in hand-applied tokens — so the discipline is encoded once and inherited everywhere, never repeated at each call site.
- The primitive owns it — pass an
elevationandSurfaceestablishes that scope and paints its base; omit it and the surface sits flush at root. For an element a<Surface>can't wrap, spreadSurface.props({ elevation, shade, bordered, shadow })onto it instead — the same data-attribute bag the component sets, applied by hand. A control-hosting surface paints its scope base (bg-background), neverbg-surface, so controls derive off that base and a ghost hover always clears it;bg-surfaceis for non-interactive content,bg-sunkenfor genuine wells.