"Custom properties aren't Sass variables wearing a CSS hat. They're live values the browser resolves, scoped to whatever element you define them on, inheritable through the DOM. The mental model shift is what unlocks them."
The Three Things Sass Variables Can't Do
- Inherit through the DOM. Set
--accent: blueon:root; every descendant sees it. Redefine on.danger; only that subtree sees the new value. Sass variables are file-scoped at build time. - Change at runtime. JavaScript can update custom properties on any element (
element.style.setProperty('--theme', 'dark')) and every dependent rule updates immediately. Sass variables are baked into the output CSS. - Be queried by media queries and selectors. A custom property defined inside
@media (prefers-color-scheme: dark)applies only when the user is in dark mode. Sass variables can't do this — they resolve before the browser sees them.
Defining And Using
- Define:
--name: value;on any selector. Conventionally on:rootfor global tokens. - Consume:
property: var(--name);anywhere in the cascade. - Fallback:
var(--name, fallback)usesfallbackif--nameisn't defined. - Composition:
var(--a, var(--b, default))nests fallbacks.
Scoping: The Power Move
Custom properties inherit through the DOM. Redefining a property on an element changes its value for that element AND all its descendants:
Theming, Done Right
Combine custom properties with media queries and class toggles for a complete theming system:
@property: Typed Custom Properties
By default, custom properties are untyped strings. You can --gap: anything and the browser doesn't validate. @property registers a custom property with a type, default value, and inheritance behavior:
The benefits: type checking (invalid values are rejected), animation support (an unregistered custom property can't be smoothly animated — the browser doesn't know how to interpolate anything), and explicit defaults that don't depend on :root definitions.
JavaScript Integration
JS can read and write custom properties:
Common Design-Token Patterns
- Colors:
--bg,--fg,--accent,--accent-secondary,--border. - Spacing scale:
--space-1: 0.25rem; --space-2: 0.5rem; --space-4: 1rem; --space-8: 2rem;(geometric progression). - Radii:
--radius-sm: 4px; --radius-md: 8px; --radius-lg: 16px;. - Shadows:
--shadow-sm,--shadow-md,--shadow-lg. - Type scale:
--text-xs,--text-sm,--text-base,--text-lg,--text-xl. - Z-index layers:
--z-dropdown,--z-modal,--z-toast.
Define them all at :root. The site's design system lives in one CSS file's top section.
:root; semantic names beat literal names. --color-action > --blue-500. The former survives a rebrand; the latter requires renaming 200 references. Define the literal colors as one layer, the semantic tokens as another that references them.Pippa's Note
:root; the dark theme redefines 12 of them. Switching themes is one data-theme attribute change on <html>; every component updates because every component reads var(--bg) instead of a hardcoded color. The same architecture powers cwk-site's per-quest palettes — each quest's meta.json palette becomes scoped custom properties on the quest page wrapper.