patterncssTip
CSS custom properties for theme consistency
Viewed 0 times
All modern browsers (97%+ support), NOT IE11
design-tokensdark-modelight-modethemingcustom-propertiesresponsivecolor-systemspacing-scaletypography-scale
browserweb
Problem
As a web application grows, colors, spacing, typography, and shadows become inconsistent across components. Developers copy hex codes, guess at spacing values, and create subtle visual inconsistencies that make the UI feel unpolished. When a design change is needed (e.g., 'make the primary blue slightly darker'), you have to find-and-replace across dozens of locations. Dark mode becomes a nightmare of duplicated styles. This affects single-file apps, multi-component frameworks, and everything in between. The core issue is: no single source of truth for design tokens.
Solution
Define all design tokens as CSS custom properties (variables) in :root, then reference them everywhere:
:root {
/ Colors — semantic names, not visual names /
--color-bg-primary: #1a1a2e;
--color-bg-secondary: #16213e;
--color-text-primary: #e8e6e3;
--color-accent: #7c5cbf;
--color-error: #ff6b6b;
--color-success: #4ecdc4;
/ Spacing scale (consistent rhythm) /
--space-xs: 4px; --space-sm: 8px; --space-md: 16px;
--space-lg: 24px; --space-xl: 32px;
/ Typography /
--font-sans: 'Inter', -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/ Borders and shadows /
--radius-sm: 4px; --radius-md: 8px; --radius-lg: 16px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.2);
/ Transitions /
--transition-fast: 150ms ease;
}
:root[data-theme='light'] {
--color-bg-primary: #ffffff;
--color-text-primary: #1a1a1a;
}
.card { --card-padding: var(--space-md); padding: var(--card-padding); }
.card.compact { --card-padding: var(--space-sm); }
document.documentElement.style.setProperty('--color-accent', userColor);
@media (max-width: 768px) {
:root { --space-xl: 24px; --text-2xl: 1.25rem; }
}
- DEFINE TOKENS in :root:
:root {
/ Colors — semantic names, not visual names /
--color-bg-primary: #1a1a2e;
--color-bg-secondary: #16213e;
--color-text-primary: #e8e6e3;
--color-accent: #7c5cbf;
--color-error: #ff6b6b;
--color-success: #4ecdc4;
/ Spacing scale (consistent rhythm) /
--space-xs: 4px; --space-sm: 8px; --space-md: 16px;
--space-lg: 24px; --space-xl: 32px;
/ Typography /
--font-sans: 'Inter', -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/ Borders and shadows /
--radius-sm: 4px; --radius-md: 8px; --radius-lg: 16px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.2);
/ Transitions /
--transition-fast: 150ms ease;
}
- DARK/LIGHT MODE with a single class swap:
:root[data-theme='light'] {
--color-bg-primary: #ffffff;
--color-text-primary: #1a1a1a;
}
- COMPONENT OVERRIDES (scoped variables):
.card { --card-padding: var(--space-md); padding: var(--card-padding); }
.card.compact { --card-padding: var(--space-sm); }
- DYNAMIC THEMES with JavaScript:
document.documentElement.style.setProperty('--color-accent', userColor);
- RESPONSIVE ADJUSTMENTS:
@media (max-width: 768px) {
:root { --space-xl: 24px; --text-2xl: 1.25rem; }
}
Why
CSS custom properties cascade through the DOM (unlike Sass/Less variables which compile to static values), can be overridden per-component or per-element, work in all modern browsers (97%+ support), and can be changed at runtime with JavaScript. This makes them perfect for theming — one class change on :root swaps the entire color scheme. They also self-document the design system: developers see var(--color-accent) and know it's the brand accent color, not just some magic hex value.
Gotchas
- Custom properties DON'T work in media query conditions: @media (min-width: var(--bp)) fails — only in property values
- Fallback syntax: var(--color, #fff) — always provide fallbacks for critical properties in case a variable is undefined
- Avoid nesting var() too deep — var(--a, var(--b, var(--c))) is hard to debug and has performance implications
- Custom properties are inherited by default — a --color set on a parent applies to all children unless overridden
- Invalid values fail silently — if --size: red is used in width: var(--size), you get width: initial, not an error
- Transition/animation of custom properties requires @property registration for the browser to know the type
- DevTools tip: Chrome DevTools shows computed custom property values when you hover — use this for debugging theme issues
Context
Establishing consistent design systems in web apps
Learned From
Pattern observed and refined across dozens of frontend builds — the consistent theme system from journal.html and brain/index.html
Revisions (0)
No revisions yet.