Skip to content

Elevation & depth

In review

Depth in Nockerl is lit from above: a neutral (or tinted-black) drop shadow plus a subtle top catch-light. No glows, no colored shadows, no emission. This is the most load-bearing of the design laws.

  • Cards lift, fields sink. A card rises off the ground: lighter surface + drop shadow + a 1px top catch-light. An input is the inverse, a recessed well: darker + inner shadow.
  • Ground is the darkest layer. The canvas is darkest; cards are a clear luminance step above. Lift comes from the gap + shadow + catch-light, never from making a card light-grey.
  • One light source. Everything is lit from the top, so highlights sit on top edges and shadows fall below. A consistent source is what makes the depth read as real.
  • The fill is static; the shadow grows. Moving a card up a tier deepens the shadow. It never lightens the surface. --color-card-surface1 is the resting fill at every rung.
The law

Depth = a neutral (or tinted-black) drop shadow + a 1px top catch-light, and nothing else. A colored box-shadow, a blurred halo, or any glow/neon is a defect. Focus is an outline (or an inset ring), never a colored shadow. See Design laws rules 1 to 3.

Two neutral tokens carry all depth, the same pair on both cards and wells:

Token Value (dark) Value (light) Role
--color-shadow-tint #000000 #1A2733 The drop / inner shadow color (mixed with transparent per rung)
--color-surface-highlight #FFFFFF0F (~6% white) #FFFFFF14 (~8% white) The 1px top catch-light

Neither is an accent hue: depth stays monochrome by construction.

Cards lift off the ground on a four-rung neutral ladder. The drop shadow is hard-edged: --elevation-blur is a constant 0 and the x offset is 0 too, so the only value that moves per rung is the downward y offset, which is what the --elevation-level* tokens hold. A higher card sits further off the ground and its shadow deepens, but it never softens into a halo. The per-rung opacity is its own token ladder, --elevation-shadow-tint-alpha-* (shadowTintAlpha, a unitless 0 to 1 number, × 100% in the web color-mix).

Offset token Y offset Alpha token Use
elevation-level1 1px shadow-tint-alpha-level1 @28% Resting cards, chips, inline rows
elevation-level2 1.5px shadow-tint-alpha-level2 @30% Standard cards, message bubbles, menus
elevation-level3 3px shadow-tint-alpha-level3 @33% Input bar, status pills, popovers, dialogs
elevation-sheet 5px shadow-tint-alpha-sheet @35% Bottom sheets, modals

This is the exact recipe the Surface / Card primitive encodes (every value is a token, so a depth change is one edit). The y offset is the rung token, the blur is the constant --elevation-blur, and the spread stays 0: a crisp shadow sitting directly under the card, never a Gaussian cloud around it.

.nk-surface {
background: var(--color-card-surface1);
border-radius: var(--radius-card); /* 16px card / 12px panel */
border: var(--space-px) solid var(--color-card-hairline);
/* the top catch-light, reused by hover/press rules */
--nk-surface-sheen: inset 0 var(--space-px) 0 var(--color-surface-highlight);
}
/* resting rung (L1) */
.nk-surface--l1 { box-shadow:
0 var(--elevation-level1) var(--elevation-blur) 0
color-mix(in srgb, var(--color-shadow-tint) calc(var(--elevation-shadow-tint-alpha-level1) * 100%), transparent), /* neutral drop */
var(--nk-surface-sheen); } /* top catch-light */
/* standard card (L2) */
.nk-surface--l2 { box-shadow:
0 var(--elevation-level2) var(--elevation-blur) 0
color-mix(in srgb, var(--color-shadow-tint) calc(var(--elevation-shadow-tint-alpha-level2) * 100%), transparent),
var(--nk-surface-sheen); }
/* raised (L3) */
.nk-surface--l3 { box-shadow:
0 var(--elevation-level3) var(--elevation-blur) 0
color-mix(in srgb, var(--color-shadow-tint) calc(var(--elevation-shadow-tint-alpha-level3) * 100%), transparent),
var(--nk-surface-sheen); }
/* sheet (L4) */
.nk-surface--l4 { box-shadow:
0 var(--elevation-sheet) var(--elevation-blur) 0
color-mix(in srgb, var(--color-shadow-tint) calc(var(--elevation-shadow-tint-alpha-sheet) * 100%), transparent),
var(--nk-surface-sheen); }

Resolving the tokens, the four rungs are (alpha from the shadowTintAlpha ladder):

/* L1 */ box-shadow: 0 1px 0 0 color-mix(in srgb, var(--color-shadow-tint) 28%, transparent), inset 0 1px 0 var(--color-surface-highlight);
/* L2 */ box-shadow: 0 1.5px 0 0 color-mix(in srgb, var(--color-shadow-tint) 30%, transparent), inset 0 1px 0 var(--color-surface-highlight);
/* L3 */ box-shadow: 0 3px 0 0 color-mix(in srgb, var(--color-shadow-tint) 33%, transparent), inset 0 1px 0 var(--color-surface-highlight);
/* L4 */ box-shadow: 0 5px 0 0 color-mix(in srgb, var(--color-shadow-tint) 35%, transparent), inset 0 1px 0 var(--color-surface-highlight);

Fields are the inverse of a card: instead of lifting off the ground they sink into it. The Well primitive (the shared home for TextField / TextArea) fills with the darker --color-canvas-alt, then presses in with an inner shadow plus the same 1px top catch-light. Nothing here lifts and nothing glows:

.nk-well {
background: var(--color-canvas-alt); /* darker than the card ground */
border: var(--space-px) solid var(--color-outline-subtle);
border-radius: var(--radius-control); /* 12px, a rounded rect, never a pill */
box-shadow:
inset 0 var(--space-0-5) var(--space-1)
color-mix(in srgb, var(--color-shadow-tint) 45%, transparent), /* inner sink shadow */
inset 0 var(--space-px) 0 var(--color-surface-highlight); /* 1px top catch-light */
}

Resolved, the resting well is:

box-shadow:
inset 0 2px 4px color-mix(in srgb, var(--color-shadow-tint) 45%, transparent),
inset 0 1px 0 var(--color-surface-highlight);

Focus keeps the well recessed and adds a cyan outline ring, a second box-shadow layer, not a glow, and not colored depth. The inner sink shadow stays; the ring hugs the control radius:

.nk-well.is-focus {
border-color: var(--color-accent-primary);
box-shadow:
inset 0 var(--space-0-5) var(--space-1)
color-mix(in srgb, var(--color-shadow-tint) 45%, transparent), /* still sunk */
0 0 0 var(--space-0-5)
color-mix(in srgb, var(--color-accent-primary) 45%, transparent); /* cyan outline ring */
}

Error / warning / success swap the border color (never the only signal, always paired with an icon + helper text); disabled and read-only flatten the inner shadow but stay visible at ≥ 3:1. The sink shadow is present in every state: a field never lifts.

Focus rings, not colored shadows

The cyan ring above is an outline-shaped box-shadow at a fixed alpha that hugs the control. It is a ring, not a blurred halo. Depth stays neutral; the only color a shadow ever carries is a focus/validation ring, and even that is an outline, not a drop shadow.

Web resolves the ladder to the box-shadow recipes above. The --elevation-* tokens are stored in px and the Compose build emits them as dp drop-shadow levels, paired with the same shadowTint + surfaceHighlight to reproduce the lit-from-above material; SwiftUI follows the same levels. The token layer is shared; only the shadow syntax is platform-native. The exact Compose / SwiftUI shadow-emission mapping is documented alongside each platform’s Surface implementation as those slices are conformed.