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 Link to this section

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 Link to this section

Four steps from scattered shadow literals to a named scale Collect, cluster, name and replace shown as four sequential stages, with a note that clustering is the judgement-heavy step and its decisions should be recorded. collect every box-shadow in the codebase cluster group near-identical values into levels name --elevation-1 … 5 by level, not by use replace codemod the literals one component at a time The clustering step is where the judgement lives A real codebase yields 30 to 60 distinct shadow values. Five levels is a decision to declare 25 of them accidental — record which literal mapped to which level so a designer can challenge a specific merge rather than the whole scale.
Tokenising elevation is mostly an act of deletion. Recording which literal collapsed into which level keeps that deletion reviewable.

1. Define a Base Elevation Scale (0–6) Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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.

The same elevation level rendered on three surface types One raised card shown on a light surface where the shadow conveys depth, on a dark surface where a lifted surface colour conveys it, and in forced-colors mode where only a border remains. Light surface shadow does the work rgba black at 8-10% reads as distance surface stays white Dark surface surface does the work black on black is invisible lift the surface instead keep a faint shadow for edges Forced colours border does the work shadows are dropped entirely by the UA declare a real border
Depth is communicated by a different property in each environment. An elevation token that only ships a shadow flattens in two of the three.

Keeping the Scale Honest After Launch Link to this section

The clustering exercise is a one-time cost; keeping the result intact is ongoing. Two habits do most of the work.

First, treat any new box-shadow literal in a review as a request for a new elevation level, and handle it as such. In most cases the answer is that an existing level is close enough and the component should use it. Occasionally the answer is that the scale is genuinely missing a step, in which case adding it deliberately — with a name, a documented role, and its z-index pairing — is far better than the alternative of a literal that nobody revisits.

Second, run the monotonicity check in CI rather than trusting review. Shadow values are long, low-contrast strings, and a transposed digit that makes level 2 heavier than level 3 is genuinely hard to spot by eye in a diff. The check costs a few lines and catches exactly the class of error that human review is worst at.

Finally, revisit the scale when the product adds a fundamentally new surface type — a persistent side panel, a floating action button, an embedded editor. These are the moments where a fifth level becomes justified, and where adding it deliberately is far cheaper than discovering six near-copies of it a year later.