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:
- 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.
- 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.
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, andcomponentshavestructured YAML schemas + lint validation; there is no analogous
structured slot for shadow / elevation tokens. This issue
proposes a
shadows:(orelevation:) token category with lintvalidation 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.mdcannot project elevation structurally — it canonly describe it in prose.
The driving example is CivTech — a browser-first AEC / CAD / BIM
editor using
@google/design.mdas a machine-readable projectionover 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-SRGBvalidation, 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:
--shadow-sm/-md/-lg),multi-layer composites for
mdandlg.--dark-shadow-sm/-md/-lg), distinct lintable tokens with darker drop + subtlewhite hairline.
(
--shadow-popover,--shadow-compass,--shadow-overlay,--shadow-callout,--shadow-toast).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 & DepthMarkdown 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 amechanical
table → YAMLtransform, not a rewrite. This issue iswhat 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 andunknown 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 lintersilently 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(orelevation— either name works;shadowsmatches the CSS
box-shadowproperty naming most directly)top-level token group, parallel in shape to
colors:Where
<Shadow>is a value type covering the five shapesdocumented below.
Shape 1 — Single-layer shadow
The minimum case. A single
box-shadowvalue as a string:The lint validation here should mirror the structural validation
colorsperforms (well-formed value, in the expected colourspace) without prescribing semantics.
Shape 2 — Composite (multi-layer) shadow
The canon ladder in CivTech composes a soft drop + a
0 0 0 0.5pxhairline ring inside a single token. The CSS uses comma-separated
layers in one
box-shadowdeclaration. The schema should acceptthis as a single token value, not require splitting into N
separate tokens (which would lose the semantic unit).
Lint should accept comma-separated layer lists as a single
<Shadow>value. Alternatively, the schema could provide anexpanded list-of-layers form per token (similar to how a
Colorvalue could be either
#RRGGBBor a structured object with rolecontext), 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 thana nested theme block. This keeps the schema flat and the
both-themes-lintable property intact:
If the schema would prefer a theme-aware structured form (e.g.,
md: { light: "...", dark: "..." }), that works too — themechanical difference is a key-prefix vs nested-object, both are
lintable.
Shape 4 — Hairline-inside-shadow semantic
The
0 0 0 0.5pxlayer inside--shadow-md/--shadow-lgissemantically a 1px border but must be drawn as a shadow
layer because
border:declarations:border-box), which means thering is included in the element's measured dimensions and
the popover/menu boundary lands somewhere the stacking-
context calculation doesn't expect.
backdrop-filter. The popover-elevated sites inCivTech use
backdrop-filter: blur(N)to dim viewportcontent under the popover; a
border:declaration on thesame 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 asill-formed shadow layers (they're valid CSS
box-shadowsyntax— spread radius 0 with offset 0 paints a uniform inset/outset
ring).
Shape 5 —
filter: drop-shadow()cross-property aliasingOne CivTech site (the compass SVG) uses
filter: drop-shadow()instead of
box-shadowbecause the geometry demands followingthe 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 siblingtoken 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 CSSregardless of property, so
filter: drop-shadow(var(--shadow-compass))and
box-shadow: var(--shadow-md)both satisfy lint againsttheir respective tokens.
Option B — Explicit property qualifier on the token.
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
colours:) #83 — "Feature: warn on unknown top-level schema keys(catch typos like
colours:)". Aligned with this request:once Feature: warn on unknown top-level schema keys (catch typos like
colours:) #83 lands, the off-specshadows:group would become ahard lint error, removing the silent-preservation behaviour.
This feature request resolves the gap by giving
shadows:alegitimate structured slot.
Tangential; touches typography sub-config, doesn't speak to
shadows.
colors level about hierarchical organisation; this issue is
purely about gaining the shadow group's existence, not its
internal hierarchy.
No existing issue tracks shadows / elevation specifically; this
appears to be a clean net-new feature request.
Precedent:
colorscategory structureThe schema's
colorsgroup is the model. It validates:#RRGGBBhex SRGB; rejects oklch, rgba, hslstrings).
(
primary,secondary, etc.) without making them required.A
shadowsgroup could mirror this:sm,popover,dark-md, etc.).box-shadowsyntax — includingmulti-layer; the lint can use a CSS-value parser or regex
pattern).
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:
Becomes:
— a one-pass
table → YAMLtransform. Notes column folds intoprose body. The committed shape is migration-ready.
Asked of the maintainers
out-of-scope for v0.2.x.
(so downstream projects can plan: prose pilot is fine for a
release or two; if the gap is multi-year, downstream tooling
decisions diverge).
of-layers per token;
shadowsvselevationfor thecategory name.
The downstream project is happy to test schema proposals
against an existing canon and report compatibility findings if
useful.