{"id":"cjga807xhflzeud","title":"CSS Custom Properties as Design Tokens That Survive Redesigns","slug":"css-custom-properties-design-tokens","summary":"Tokens in JavaScript objects die on framework change. Tokens on :root cascade through vanilla CSS and every shell I ship.","imageUrl":"https://briancrabtree.me/images/journal-css-custom-properties-design-tokens.webp","category":"CSS","date":"2026-04-27T18:00:00.000Z","featured":false,"likes":17,"author":"Brian Crabtree","content":"<h2>Why variables beat Sass constants alone</h2>\n\n<p>Sass variables compile away. Custom properties cascade at runtime, inherit into shadow DOM with limits, and swap per theme with one attribute toggle. I define semantic tokens: --color-surface, --color-text, not --blue-500 copied in components.</p>\n\n<p>Primitives live on :root; components reference semantics so rebrand changes one layer.</p>\n\n<p>calc() with variables enables spacing that respects grid baseline. I document formula once in tokens file comment. Fallback values in var(--token, fallback) protect older browsers during token rollout.</p>\n\n<pre><code>:root {\n  --text-main: 240 240 240;\n  --acid-main: 204 255 0;\n  --space-md: 1rem;\n}\n.btn { background: rgb(var(--acid-main)); }</code></pre>\n\n<h2>Spacing, type, and motion</h2>\n\n<p>Modular scale for font-size tokens, spacing scale for margin and padding, duration tokens for transitions. I avoid animating layout properties; transform and opacity respect user prefers-reduced-motion when media query sets --motion-safe to 0.</p>\n\n<p>Prefers-color-scheme media query can seed default theme before localStorage read for first visit without flash.</p>\n\n<p>Z-index tokens as --z-modal prevent random 9999 values in feature branches.</p>\n\n<h2>Theme switching without flash</h2>\n\n<p>data-theme on html set inline in head from localStorage before paint prevents flash of wrong theme. Server can read cookie and emit correct theme on first byte for marketing pages.</p>\n\n<p>Dark mode is not invert filter. Re-tune surfaces and borders; contrast ratios change.</p>\n\n<p>Component tokens alias semantic tokens: --button-bg: var(--color-accent). Changing accent ripples correctly. Focus ring tokens unify outline color across web components and vanilla buttons.</p>\n\n<h2>Interop with React and build tools</h2>\n\n<p>Import tokens.css once globally. Components use var() in modules. No need to thread theme object through context unless dynamic per-user theming is product core.</p>\n\n<p>Tailwind can map theme.extend.colors to var references. Best of both when team insists on utilities.</p>\n\n<p>PostCSS is optional when browsers target evergreen. Native nesting in CSS files reduces build step count. Print stylesheet can remap tokens to grayscale without touching component rules.</p>\n\n<h2>Journal palettes example</h2>\n\n<p>Backfill journal headers use named palettes: slate, ocean, sand. Each maps to generated WebP headers via script reading palette key. Tokens stay centralized; art direction varies per post without new CSS files per slug.</p>\n\n<p>Designers export Figma variables; I map to CSS with style-dictionary when scale is enterprise, manual when small.</p>\n\n<p>Storybook docs page listing tokens helps designers self-serve without Slack pings.</p>\n\n<h2>Governance</h2>\n\n<p>Token table in README with usage rules. Lint against raw hex in components during review. Exceptions need comment why.</p>\n\n<p>Lint stylelint declaration-property-value-disallowed-list for raw hex in component scope.</p>\n\n<p>Breaking token rename should codemod in repo, not manual find in Figma only.</p>\n\n<h2>Migration path</h2>\n\n<p>Extract top twenty colors and spacing values from Figma into :root, replace in three templates, ship. Expand weekly. Tokens pay rent on the second redesign when competitors rewrite components.</p>\n\n<p>Tokens in email templates need inline fallback because webmail strips style tags sometimes. Separate pipeline.</p>\n\n<p>Runtime theming per user on marketing site is rare; do not build unless product requires it. For a related angle I keep coming back to, see <a href=\"/journal/wcag-contrast-brutalist-dark-ui/\">WCAG Color Contrast for Brutalist and Dark UIs</a>.</p>","tags":["css-variables","tokens","design-system"],"views":58}