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.
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). - Stylelint —
stylelint-declaration-property-value-disallowed-listplugin installed to block rawbox-shadowandz-indexvalues 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
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:
- Token resolution in DevTools — open the Computed panel for a
.modalelement. Thebox-shadowvalue should be the resolved string from--elevation-4, not the literalvar()text. If you seevar(--elevation-modal)in Computed, the token file loaded after paint; check stylesheet order. - 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. - Visual regression diff — run
npx playwright test --update-snapshotsonce against a known-good state, then re-run without the flag on subsequent PRs. Any diff in shadow area indicates an unintended token change. - Stylelint gate — run
npx stylelint "src/**/*.css". Expect zero violations forbox-shadoworz-indexraw 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.
-
Audit — extract all hardcoded
box-shadow,z-index, andtranslateZvalues 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)'"] } -
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.
-
Refactor — apply codemods to replace inline values with
var(--elevation-*)references. Preserve legacy fallbacks during the transition period using CSS@layeror feature queries:npx jscodeshift -t ./codemods/elevation-tokenizer.js src/ --extensions=tsx,css -
Verify — run parallel builds with legacy and tokenized outputs. Compare computed styles using Playwright or visual regression suites to confirm zero optical regression.
-
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.
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.
Related Link to this section
- Elevation & Shadow Tokens — parent reference covering the full shadow token taxonomy and naming model
- Color Palette Architecture — the same primitive-to-semantic aliasing pattern applied to color tokens
- Token Fundamentals & Naming Conventions — section overview covering the three-tier token hierarchy that elevation tokens sit within