Tokenizing Elevation Values for Consistent Depth

Part of Elevation & Shadow Tokens. Establishing a predictable depth hierarchy requires decoupling visual representation from implementation logic — specifically, abstracting z-index stacking contexts, box-shadow definitions, and transform-based parallax into a unified token registry so engineering teams eliminate visual drift across component libraries.

Elevation depth ramp from level 0 to level 6 Seven stacked card shapes rising in visual depth from left to right, colored in violet with increasing shadow spread and opacity to represent elevation levels 0 through 6. Increasing depth → Level 0 none Surface Level 1 1px 3px Card Level 2 4px 6px Dropdown Level 3 10px 15px Overlay Level 4 20px 25px Modal Level 5 25px 50px Toast Level 6 35px 60px Dragging --elevation-0 --elevation-6
Elevation depth ramp: seven levels mapped from primitive shadow tokens to semantic roles, with blur and spread increasing at each step.

Prerequisites

Before implementing elevation tokens, confirm the following are in place:

  • Token tier structure — your project separates primitive tokens (raw values) from semantic tokens (contextual aliases). See the three-tier naming model for the canonical approach.
  • Token compiler — Style Dictionary v3+ (or equivalent: Theo, Cobalt) configured to resolve recursive alias chains.
  • CSS custom property support — all target browsers support var() (all evergreen browsers do; IE 11 requires a PostCSS fallback or polyfill).
  • Stylelintstylelint-declaration-property-value-disallowed-list plugin installed to block raw box-shadow and z-index values outside the token layer.
  • CI visual regression baseline — Playwright or Chromatic snapshots covering at least the surface, card, and modal elevation states.

Step-by-Step Implementation

1. Define a Base Elevation Scale (0–6)

Map primitive shadow values to CSS custom properties in a centralized registry. Use a consistent mathematical progression for blur, spread, and opacity to maintain optical balance.

/* tokens/primitives.css */
:root {
  --elevation-0: none;
  --elevation-1: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
  --elevation-2: 0 4px 6px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04);
  --elevation-3: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
  --elevation-4: 0 20px 25px rgba(0, 0, 0, 0.12), 0 10px 10px rgba(0, 0, 0, 0.06);
  --elevation-5: 0 25px 50px rgba(0, 0, 0, 0.18), 0 12px 12px rgba(0, 0, 0, 0.08);
  --elevation-6: 0 35px 60px rgba(0, 0, 0, 0.22), 0 15px 15px rgba(0, 0, 0, 0.1);
}

--elevation-0 uses none rather than 0 0 0 0 rgba(0,0,0,0). While the latter is technically valid, none is more readable and equally safe to transition from/to in modern browsers.

2. Alias Primitives to Semantic Tokens

Decouple the scale from usage intent. Map scale values to contextual roles consumed by components — the same pattern used in color palette architecture where raw palette entries alias to semantic roles like --color-bg-surface.

/* tokens/semantic.css */
:root {
  --elevation-surface: var(--elevation-1);
  --elevation-card:    var(--elevation-2);
  --elevation-overlay: var(--elevation-3);
  --elevation-modal:   var(--elevation-4);
  --elevation-toast:   var(--elevation-5);
  --elevation-dragging: var(--elevation-6);
}

Components reference semantic tokens (--elevation-card) rather than primitives (--elevation-2), so a single edit to the alias repoints every consumer without touching component CSS.

3. Implement Fallback Chains

Use explicit fallback values in component CSS to prevent unstyled states when tokens fail to resolve — for example, when a component is loaded in an environment that hasn’t yet parsed the token file.

.component-elevated {
  /* Fallback to --elevation-2 value if --elevation-card is missing */
  box-shadow: var(--elevation-card, 0 4px 6px rgba(0, 0, 0, 0.08));
}

The fallback is a literal primitive value, not another var(). A var() inside a fallback resolves lazily and can silently produce an invalid value if that token is also undefined.

4. Standardize Blur, Spread, and Opacity Ratios

Integrate with the token compiler to lock shadow geometry across responsive breakpoints. Configure output transforms to emit breakpoint-specific overrides where optical scaling is needed — for instance, high-DPI displays often warrant slightly reduced blur to avoid muddiness.

{
  "elevation": {
    "4": {
      "value": "0 20px 25px rgba(0,0,0,0.12), 0 10px 10px rgba(0,0,0,0.06)",
      "type": "boxShadow",
      "attributes": {
        "category": "elevation",
        "level": 4,
        "role": "modal"
      }
    }
  }
}

Including type and attributes lets the compiler generate platform-specific outputs (Android elevation dp, iOS shadowRadius) from the same source.

5. Validate Token Resolution in Theme Modes

Run automated computed style assertions. Ensure prefers-color-scheme or data-attribute themes correctly invert shadow opacity without breaking stacking contexts.

// playwright/elevation.spec.js
test('modal elevation resolves correctly in dark mode', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'dark' });
  await page.goto('/components/modal');
  const shadow = await page.locator('.modal').evaluate(
    el => getComputedStyle(el).boxShadow
  );
  expect(shadow).not.toBe('none');
  expect(shadow).toContain('rgba');
});

Verification

After completing the implementation steps, confirm the following:

  1. Token resolution in DevTools — open the Computed panel for a .modal element. The box-shadow value should be the resolved string from --elevation-4, not the literal var() text. If you see var(--elevation-modal) in Computed, the token file loaded after paint; check stylesheet order.
  2. CI log pattern — a successful build with full alias resolution produces output like:
    [INFO] Style Dictionary: 6 primitive elevation tokens registered.
    [INFO] Style Dictionary: 6 semantic elevation aliases resolved.
    [SUCCESS] Token AST validated. 42 semantic aliases resolved.
  3. Visual regression diff — run npx playwright test --update-snapshots once against a known-good state, then re-run without the flag on subsequent PRs. Any diff in shadow area indicates an unintended token change.
  4. Stylelint gate — run npx stylelint "src/**/*.css". Expect zero violations for box-shadow or z-index raw values in component files.

Troubleshooting

Symptom Likely Cause Fix
undefined CSS variable in build output Token compiler fails to resolve recursive alias chain; likely a missing or misnamed primitive. Run npm run tokens:build -- --verbose to inspect intermediate output. Ensure every alias terminates at a primitive value. Enforce JSON schema validation with npx ajv validate -s schema.json -d tokens.json before compilation.
Visual regression flags depth mismatches despite passing unit tests Browser-specific shadow rendering differences (WebKit vs Blink) or missing @supports fallbacks for complex box-shadow stacks. Inject computed style snapshots into CI artifacts. Use Playwright getComputedStyle() assertions to validate exact box-shadow strings against token baselines.
Token drift detected in PR reviews Manual overrides in component CSS bypassing the token registry (e.g., inline z-index: 9999 or hardcoded shadows). Enforce Stylelint declaration-property-value-disallowed-list to block raw box-shadow and z-index values outside the token layer. Fail PRs on violation.
Shadows disappear in forced-colors mode Windows High Contrast Mode strips box-shadow entirely. Replace with outline under @media (forced-colors: active). Elevation intent is communicated through border/outline, not shadow.
Dark-mode shadows appear too harsh Primitive opacity values calibrated for light backgrounds only. Add a [data-theme="dark"] :root override block that reduces the alpha channel on each primitive by ~30–40%, then re-alias semantics to the same token names.

Migration Note

Migrating legacy elevation implementations requires a phased, automated approach to prevent regression and maintain developer velocity.

  1. Audit — extract all hardcoded box-shadow, z-index, and translateZ values using AST parsing. Run a Stylelint pass across the codebase to generate an inventory report:

    {
      "components/Modal.css": ["box-shadow: 0 10px 30px rgba(0,0,0,0.15)", "z-index: 1000"],
      "components/Card.tsx": ["boxShadow: '0 4px 12px rgba(0,0,0,0.08)'"]
    }
  2. Map — create a translation matrix matching legacy values to the nearest semantic elevation token. Use perceptual distance algorithms or manual design review to align legacy shadows with the new scale.

  3. Refactor — apply codemods to replace inline values with var(--elevation-*) references. Preserve legacy fallbacks during the transition period using CSS @layer or feature queries:

    npx jscodeshift -t ./codemods/elevation-tokenizer.js src/ --extensions=tsx,css
  4. Verify — run parallel builds with legacy and tokenized outputs. Compare computed styles using Playwright or visual regression suites to confirm zero optical regression.

  5. Deprecate — remove legacy shadow definitions and enforce token-only usage via CI lint gates and pre-commit hooks (husky + lint-staged). Archive the translation matrix for historical reference.


Cross-Environment Validation & Maintenance

Maintain elevation consistency by integrating token validation into pre-commit hooks and CI pipelines. Use token diffing tools to detect unauthorized scale modifications and enforce semantic naming conventions. Document resolution patterns for edge cases, including high-DPI display scaling, prefers-reduced-motion constraints, and forced-colors mode compliance. Regularly audit token consumption metrics to identify unused or duplicated elevation references, ensuring the registry remains lean, accessible, and predictable across all delivery channels.