Lighting & shadow model
The light source model behind every shadow in Koder Design. Defines WHERE light comes from (key light position + ambient), HOW shadows are cast (umbra/penumbra, direction, softness), and how dark mode RE-LIGHTS rather than just dimming shadows. Elevation tokens (`elevation.kmd`) are the rendered output of this model; this spec is the source they derive from. Material parity: the implicit light model behind M3 elevation, made explicit and tokenized.
Facet Visual do Koder Design. Material parity: the light model implicit in https://m3.material.io/styles/elevation — Koder makes it explicit so shadows across 2D, 3D and XR stay coherent.
Elevation says which shadow a surface gets. Lighting says why that shadow looks the way it does — one virtual light rig, so a card on the web, a model-viewer mesh, and an XR panel all cast consistent shadows. Every --shadow-* token in elevation.kmd is a 2D projection of this model.
R1 — The light rig
Koder uses a two-source rig (Material/RealityKit convention):
| Source | Role | Default |
|---|---|---|
| Key light | Directional; casts the visible shadow | From top, slightly front and left: azimuth −30°, elevation 60° |
| Ambient light | Omnidirectional fill; lifts shadow floor | Soft, ~40% intensity; no direction |
Consequence (canonical): shadows fall down and slightly right of the element. Every preset shadow recipe MUST be consistent with this single direction — no per-element light angles (elevation.kmd § R5).
R2 — Shadow anatomy
A cast shadow is two layers, both tokenized:
| Layer | Purpose | Driven by |
|---|---|---|
| Umbra (key shadow) | Tight, darker contact shadow | Depth level + key-light hardness |
| Penumbra (ambient shadow) | Wide, soft diffusion | Depth level + ambient intensity |
This is exactly why elevation.kmd recipes are two box-shadow layers (a tight one + a wide one). Higher depth = larger offset + larger blur on both layers (object is farther from the surface it casts onto).
R3 — Softness tokens
| Token | Meaning | Default |
|---|---|---|
--kds-light-azimuth |
Horizontal light angle (deg) | -30 |
--kds-light-elevation |
Vertical light angle (deg) | 60 |
--kds-light-hardness |
Umbra sharpness 0(soft)–1(hard) |
0.4 |
--kds-ambient-intensity |
Penumbra strength 0–1 |
0.4 |
Presets re-author the rig, not individual shadows:
| Preset | Rig change |
|---|---|
verge (default) |
Canonical rig above |
macos_sonoma |
Softer: hardness 0.2, longer penumbra |
material_expressive |
Stronger key: hardness 0.6 |
windows_95 / brutalist / terminal_classic |
Light rig off — bevel/flat per elevation.kmd § R4 |
neumorphism |
Dual light (key + opposite fill) for inset/extrude |
R4 — Dark mode is re-lighting, not dimming
The common mistake: in dark mode, keep the same shadow but lower its opacity. Wrong — on a dark surface a darker shadow is invisible.
Koder dark mode re-lights:
- Key shadow stays (darker, larger blur —
elevation.kmd § R2dark recipe) but contributes little.
- Tonal overlay becomes the primary depth cue: the accent-tinted
surface lightens with depth (the object catches more key light the closer it is). This is the
--tonal-*system inelevation.kmd.
So: light mode = shadow-driven, dark mode = light-catch-driven (tonal lift). Same physical model, opposite dominant cue.
R5 — 3D & XR cast shadows
For real-geometry surfaces (components/model-viewer.kmd, XR panels):
- The same rig (R1 azimuth/elevation) drives the renderer's
directional light — a glTF mesh and a CSS card are lit from the same angle.
- XR shadow distance (
--kds-xr-shadow-distance,xr-preview.kmd § R3)is the spatial projection of
--kds-light-elevation+ depth level. - Contact shadow under a floating XR panel uses the umbra; the soft
ground shadow uses the penumbra.
R6 — Forbidden patterns
- ❌ Per-element light direction (breaks the single-rig illusion).
- ❌ Shadows that fall up (light from below — uncanny except
deliberate
neumorphisminset). - ❌ Colored shadows other than the system's near-black/tonal pair
(
elevation.kmd § R5). - ❌ Dark-mode shadow that only dims opacity with no tonal lift (R4).
- ❌ Hard-coded light angles in a renderer; always read the rig tokens.
R7 — Accessibility & forced colors
- Light/shadow is never the sole signal of state or depth
(
elevation.kmd § R7). - Under
forced-colors: active, the rig is inert — depth falls backto borders (
elevation.kmd § R6). prefers-reduced-transparencydoes not affect lighting (it affectsmaterials.kmd), but reduced-motion freezes any animated light (e.g. drag-follow highlights).
T1-T3 — Tests
T1 — Single direction: across all canonical presets, every resolved --shadow-* recipe casts in the R1 direction (offset-y > 0, |offset-x| consistent sign). koder-spec-audit lighting --direction.
T2 — Dark re-light: in dark mode each depth level resolves a non-zero --tonal-* overlay (proves light-catch cue exists, not just dimming).
T3 — Rig coherence: a model-viewer directional light angle equals --kds-light-azimuth/elevation within 1°.
Cross-link
themes/elevation.kmd— the 2D rendered output of this modelthemes/depth.kmd— the abstract z-axis this model illuminatesthemes/light-dark.kmd— light/dark cue balance (R4)components/model-viewer.kmd— real-geometry lighting (R5)develop/xr-preview.kmd— spatial shadow projection (R5)