ADR-021 — Consolidate container.default/page into container.canvas
Date: 2026-08-06
Status: accepted
Context
color.background.container carried four values: default (→ surface.1), page (→ surface.2), elevated (→ surface.3), inverted. An audit found default was a false friend — no Box in the app or Storybook ever set background="default"; the only real consumer was Card’s resting-state variant. Everything that wanted “the page’s own surface color” reached for page, but Box’s background prop never exposed page at all — it only offered default | inverted | transparent | elevated. The gap was papered over by a hand-rolled .pageSurface utility class in utilities.module.css, duplicating what Box should have done natively.
Separately, page as a prop value read ambiguously against Box’s structural as="main" prop — background="page" looks like it’s describing a layout role (the page wrapper) when it’s actually a color role (this surface matches the page’s background). background.neutral.* (subtle/subtlest/hover) is a distinct, already-separate token family for alpha-tinted fills (Badge, Chip, ButtonArrow, ProgressBar), not part of this surface-level ramp — conflating the two was a source of confusion but not a structural problem.
Decision
- Merge
container.defaultandcontainer.pageinto a single token,container.canvas, keepingpage’s value (surface.2/neutral.dark.2) — the color already live everywhere viareset.cssand.pageSurface— sincedefault(surface.1/#FEFDF9) had no real caller. - Expose it as
Box’sbackground="canvas", replacing the.pageSurfaceworkaround.Box’sbackgroundunion is nowcanvas | elevated | inverted | transparent— every value has exactly one job and no false friends. - Name it
canvas, notpage, to keep the color axis (background) and the structural axis (as="main") unambiguous in code. - Card’s default variant and every other
container-default/container-pageCSS reference now points atcontainer-canvas.
Consequences
- One fewer token in the surface ramp (4 → 3 named container levels, plus
transparent), matching common-practice surface hierarchies (canvas → surface → elevated) rather than a wider, unused set. - The contrast checker’s “base canvas” composite target moved from the effectively-unrendered
surface.1to the realsurface.2, surfacing two genuine near-miss pairs (text.selected/text.brandonneutral.subtlest/neutral.hover, ~4.46–4.47:1 vs 4.5:1) that were never actually checked against their true rendering context before. Tracked as waivers, issue #96 — not a regression from this change, the check becoming accurate. - Figma’s variable is still named “Page” under Color > Background > Container; propagating the
canvasrename there is a/figma-variable-pushfollow-up, not done as part of this change (code is the source of truth per ADR-002). background.neutral.*is unchanged — confirmed as intentionally separate from the container/surface ramp, not folded in.
Amendment (2026-08-06)
background.neutral.* was renamed to background.overlay.* in a follow-up pass — “neutral” named a hue family, but these tokens (subtle/subtlest/hover, all alpha-transparent) are functionally overlay/scrim tints, not a resting surface color. Pure rename, no value changes; all CSS/metadata usages and the contrast-waivers ledger were updated to match. Doesn’t affect the container.canvas decision above.