Custom properties (commonly known as CSS variables) give "magic numbers" and repeated color values a single source of truth. This tutorial site's color system — paper, ink, and accent colors — is defined entirely through custom properties on :root; switching between light and dark themes is essentially reassigning those variables.
Custom properties start with --, can hold any valid CSS fragment as their value, and are read with the var() function. They are case-sensitive, and the browser performs no syntax validation on them (until they are actually used in a var()).
Custom Properties Cascade Too, and Inherit
Like ordinary properties, custom properties follow the cascade and inheritance rules — that is exactly what makes them more powerful than Sass variables: Sass variables are replaced with fixed values at compile time, while CSS variables take effect at runtime, layer by layer down the DOM tree, and can be overridden by selectors at any level.
.theme-warm { --accent: #b3452c; }
/* every element inside .theme-warm that references --accent switches color automatically, with no redeclaration needed */
Live preview: locally overriding a variable
default --accent
after local override
Fallback Values
The second argument of var() is the fallback value, used when the variable is undefined, so components do not break when taken out of their theme context:
Custom properties can be read and written by JavaScript, making them the bridge between the style system and interaction logic — no need to concatenate long style strings with inline styles:
To the browser, an ordinary custom property's value is just "unparsed text", so it cannot participate in transition animations (for example, gradient angles and colors cannot transition smoothly). @property lets us explicitly declare the variable's type, initial value, and whether it inherits, giving it the ability to take part in interpolation:
In large projects, organize custom properties into a layered "design token" system: the bottom layer holds raw colors and spacing (primitive), the top layer holds semantic aliases (semantic), and component styles only reference the semantic layer — retheming only requires changing the top-level mappings: