Skip to content

Feature: structured shadows / elevation token category #92

@theja0143

Description

@theja0143

Summary

The current spec (v0.1.1 / v0.2.0 alpha) treats the Elevation &
Depth
section as prose-only. Token groups for colors,
typography, rounded, spacing, and components have
structured YAML schemas + lint validation; there is no analogous
structured slot for shadow / elevation tokens. This issue
proposes a shadows: (or elevation:) token category with lint
validation analogous to colors.

Use case

A real-world canon needs to document its elevation language as
structured tokens, the same way it documents its colour language.
Today, design.md cannot project elevation structurally — it can
only describe it in prose.

The driving example is CivTech — a browser-first AEC / CAD / BIM
editor using @google/design.md as a machine-readable projection
over its design canon. (Source not yet public; happy to share
canon snippets in-thread if useful.) Colors
landed cleanly using the structured colors: group with hex-SRGB
validation, which catches drift between the projection and the
authoritative CSS. The next adoption step is shadows, but the
lint-catchable-drift protection that made the colors projection
worth committing does not currently extend to shadows under the
alpha schema.

CivTech's elevation surface, as a concrete shape the schema would
need to admit:

  • 3 canon shadow tokens (--shadow-sm / -md / -lg),
    multi-layer composites for md and lg.
  • 3 proposed dark variants (--dark-shadow-sm / -md /
    -lg), distinct lintable tokens with darker drop + subtle
    white hairline.
  • 5 promoted semantic-role tokens for cinematic one-offs
    (--shadow-popover, --shadow-compass, --shadow-overlay,
    --shadow-callout, --shadow-toast).
  • ~4 documented canon-escape exceptions that stay as one-off
    CSS values intentionally not promoted to canon tokens.

Total proposed token count: 11. Small surface, but each
token carries structural intent (composite layers + hairline
border-substitute + cross-property aliasing) that the schema
needs to admit for the projection to do its job.

The downstream pilot has already shipped a prose-only
## Elevation & Depth Markdown section as a half-value delivery
(Claude Code structured vocabulary, no lint protection), with an
honest-framing preamble noting that lint enforcement is pending
upstream schema support. The prose section's table is intentionally
shaped so a future migration to a shadows: YAML group is a
mechanical table → YAML transform, not a rewrite. This issue is
what unblocks that migration.

Why this isn't already covered by "prose for unknown sections"

The spec's documented behaviour for unknown content (Consumer
Behavior table in spec.md) preserves unknown sections and
unknown token groups silently. An empirical probe against both
v0.1.1 and v0.2.0 confirms: a top-level shadows: { sm:..., md:..., lg:... } YAML group lints to 0 errors but the linter
silently preserves the group without validating any values
.
Multi-layer composite values pass; single-layer values pass;
ill-formed strings would also pass — nothing inspects them.

This is a lying-affordance pattern. Future readers (humans
and AI coding agents alike) reasonably assume the structured
YAML shape implies lint validation in the same way colors:
does. The downstream CivTech pilot explicitly rejected the
off-spec shadows: hedge for that reason.

Proposed shape

A shadows (or elevation — either name works; shadows
matches the CSS box-shadow property naming most directly)
top-level token group, parallel in shape to colors:

shadows:
  <token-name>: <Shadow>

Where <Shadow> is a value type covering the five shapes
documented below.

Shape 1 — Single-layer shadow

The minimum case. A single box-shadow value as a string:

shadows:
  sm: "0 1px 0 rgba(0,0,0,0.03)"

The lint validation here should mirror the structural validation
colors performs (well-formed value, in the expected colour
space) without prescribing semantics.

Shape 2 — Composite (multi-layer) shadow

The canon ladder in CivTech composes a soft drop + a 0 0 0 0.5px
hairline ring inside a single token. The CSS uses comma-separated
layers in one box-shadow declaration. The schema should accept
this as a single token value, not require splitting into N
separate tokens (which would lose the semantic unit).

shadows:
  md: "0 1px 2px rgba(20,15,10,0.06), 0 0 0 0.5px rgba(20,15,10,0.06)"
  lg: "0 12px 40px rgba(20,15,10,0.18), 0 0 0 0.5px rgba(20,15,10,0.10)"

Lint should accept comma-separated layer lists as a single
<Shadow> value. Alternatively, the schema could provide an
expanded list-of-layers form per token (similar to how a Color
value could be either #RRGGBB or a structured object with role
context), but the CSS-string form is the migration-cheapest path
from existing canon and easiest for tooling round-trips.

Shape 3 — Dark-theme parallel tokens

Following the colors-pilot pattern that the downstream project
adopted (and which Claude Design ratified), dark-theme tokens
are distinct lintable entries with a dark- prefix rather than
a nested theme block. This keeps the schema flat and the
both-themes-lintable property intact:

shadows:
  sm: "0 1px 0 rgba(0,0,0,0.03)"
  md: "..."
  lg: "..."
  dark-sm: "0 1px 0 rgba(0,0,0,0.30)"
  dark-md: "0 1px 2px rgba(0,0,0,0.35), 0 0 0 0.5px rgba(255,255,255,0.06)"
  dark-lg: "0 12px 40px rgba(0,0,0,0.55), 0 0 0 0.5px rgba(255,255,255,0.10)"

If the schema would prefer a theme-aware structured form (e.g.,
md: { light: "...", dark: "..." }), that works too — the
mechanical difference is a key-prefix vs nested-object, both are
lintable.

Shape 4 — Hairline-inside-shadow semantic

The 0 0 0 0.5px layer inside --shadow-md / --shadow-lg is
semantically a 1px border but must be drawn as a shadow
layer because border: declarations:

  1. Sit inside the layout box (border-box), which means the
    ring is included in the element's measured dimensions and
    the popover/menu boundary lands somewhere the stacking-
    context calculation doesn't expect.
  2. Break backdrop-filter. The popover-elevated sites in
    CivTech use backdrop-filter: blur(N) to dim viewport
    content under the popover; a border: declaration on the
    same element creates a compositing seam at the border edge
    where the blur is masked. A shadow-layer hairline sits in
    the outer compositing layer and avoids this conflict.

The schema doesn't need to encode this semantic; just accept
composite values with a hairline layer as valid <Shadow>
values
. The semantic stays in prose / documentation, but the
schema must not reject 0 0 0 0.5px rgba(...) patterns as
ill-formed shadow layers (they're valid CSS box-shadow syntax
— spread radius 0 with offset 0 paints a uniform inset/outset
ring).

Shape 5 — filter: drop-shadow() cross-property aliasing

One CivTech site (the compass SVG) uses filter: drop-shadow()
instead of box-shadow because the geometry demands following
the visual silhouette of an SVG rather than the bounding box.
The numeric values follow the same elevation register as
--shadow-md's primary layer; it's semantically a sibling
token but lives on a different CSS property.

Two possible schema accommodations:

Option A — Property-agnostic lint matching. Lint matches
<token-name>: <Shadow> against any value occurrence in CSS
regardless of property, so filter: drop-shadow(var(--shadow-compass))
and box-shadow: var(--shadow-md) both satisfy lint against
their respective tokens.

Option B — Explicit property qualifier on the token.

shadows:
  compass:
    property: drop-shadow
    value: "0 4px 12px rgba(0,0,0,0.4)"

Option A is structurally simpler; Option B is more explicit.
Either resolves the cross-property aliasing concern; option A
is preferred
because it doesn't require token consumers to
know which property they're consuming the token through.

Related upstream issues observed

No existing issue tracks shadows / elevation specifically; this
appears to be a clean net-new feature request.

Note on #83 interaction. design.md projects relying on
schema validation must currently avoid off-spec top-level
keys for surfaces not yet supported; if #83 lands, this
becomes a hard CI error. A first-class shadows: (or
generalised elevation:) category lets canon document
elevation without inventing off-spec extensions.

Precedent: colors category structure

The schema's colors group is the model. It validates:

  • Token name shape (kebab-case identifier).
  • Value shape (#RRGGBB hex SRGB; rejects oklch, rgba, hsl
    strings).
  • Lint emits warnings/infos for missing standard tokens
    (primary, secondary, etc.) without making them required.

A shadows group could mirror this:

  • Token name shape (kebab-case identifier — sm, popover,
    dark-md, etc.).
  • Value shape (well-formed CSS box-shadow syntax — including
    multi-layer; the lint can use a CSS-value parser or regex
    pattern).
  • Lint emits warnings/infos for missing standard tokens (sm,
    md, lg) without making them required.

What the downstream pilot has already committed to this shape

The CivTech prose-only pilot ships the elevation section as a
Markdown table with Token / Value / Notes columns. The
migration path to YAML when the schema lands this category is
mechanical:

| Token | Value | Notes |
|-------|-------|-------|
| `--shadow-sm` | `0 1px 0 rgba(0,0,0,0.03)` | Subtle single-pixel separation; rule lines + dense rows. Single-layer. |
| `--shadow-md` | `0 1px 2px rgba(20,15,10,0.06), 0 0 0 0.5px rgba(20,15,10,0.06)` | Composite. Soft drop + 0.5px warm hairline ring. |
| `--shadow-lg` | `0 12px 40px rgba(20,15,10,0.18), 0 0 0 0.5px rgba(20,15,10,0.10)` | Composite. Larger soft drop + slightly stronger hairline. |

Becomes:

shadows:
  sm: "0 1px 0 rgba(0,0,0,0.03)"
  md: "0 1px 2px rgba(20,15,10,0.06), 0 0 0 0.5px rgba(20,15,10,0.06)"
  lg: "0 12px 40px rgba(20,15,10,0.18), 0 0 0 0.5px rgba(20,15,10,0.10)"

— a one-pass table → YAML transform. Notes column folds into
prose body. The committed shape is migration-ready.

Asked of the maintainers

  • Acknowledgement that the shadows/elevation token category is
    out-of-scope for v0.2.x.
  • Indication of whether this is a candidate for v0.3 or later
    (so downstream projects can plan: prose pilot is fine for a
    release or two; if the gap is multi-year, downstream tooling
    decisions diverge).
  • Preferred shape — single-string CSS value vs structured list-
    of-layers per token; shadows vs elevation for the
    category name.

The downstream project is happy to test schema proposals
against an existing canon and report compatibility findings if
useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions