Noctis

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.

bg-sunken at rootbg-sunken
bg-sunken in the sunken scopedata-elevation="sunken" · bg-sunken

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.

bg-sunken
bg-background
bg-hover
bg-surface
bg-surface-hover
bg-surface-raised

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.

wellbasehoversurfacesurface+raised
menu
elevated
root
sunken

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.

SurfaceScopePaints with
Page canvas / app backgroundrootbg-background
Cards, content panelsrootbg-surface
Content wells, code blocks, insets, scrollbar trackrootbg-sunken
Sidebar, Rail (page chrome)rootbg-background
Dialog, Sheet, Selectelevation="elevated"bg-background
Dropdown (Menu), Context menu, Command palette (Search dialog), Popover, Tooltipelevation="menu"bg-background
Buttons, menu itemsanybg-control
Inputs, text fieldsanybg-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 elevation and Surface establishes that scope and paints its base; omit it and the surface sits flush at root. For an element a <Surface> can't wrap, spread Surface.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), never bg-surface, so controls derive off that base and a ghost hover always clears it; bg-surface is for non-interactive content, bg-sunken for genuine wells.