CSS Custom Properties: Scoped Theming Without Preprocessors
Use CSS custom properties for dynamic theming, inheritance, and fallbacks without a preprocessor build step.
04 Jul 2026, 18:55 UTC

The problem: hardcoded values and static themes
When a design system repeats the same brand color, spacing unit, or border radius across dozens of rules, changing one value means find-and-replace across many selectors. Preprocessors solve this at build time, but they cannot react to user preferences, dark mode toggles, or component state after the page loads.
CSS custom properties—also called CSS variables—solve this by storing values in the browser itself. You declare a property such as --brand-color, reference it with var(--brand-color), and update it at runtime. Because the value lives in CSS, every rule that uses it updates immediately when the variable changes.
How custom properties work
A custom property starts with two dashes and can be set on any element. Authors usually put global values on :root, which represents the document root and behaves like html with higher specificity. Consumers read the value with the var() function, optionally providing a fallback if the variable is undefined.
Custom properties follow the normal CSS cascade and inheritance. A value set on a parent element is available to descendants, and a child element can override the same property name for its own subtree without fighting selector specificity.
Worked configuration: component-scoped theme
:root {
--brand-color: #1a73e8;
--surface-color: #ffffff;
--spacing-unit: 1rem;
}
.card {
background: var(--surface-color);
border: 1px solid var(--brand-color);
border-radius: 0.5rem;
padding: var(--spacing-unit);
}
.card--prominent {
/* Scope a new value to this component and its children */
--brand-color: #d93025;
--surface-color: #fce8e6;
}
.button {
background: var(--brand-color, #555555);
color: var(--surface-color, #ffffff);
padding: calc(var(--spacing-unit) * 0.5);
border: none;
}
In this example, .card uses the global --brand-color and --surface-color. The modifier .card--prominent overrides those variables for itself and any nested elements, so a .button inside a prominent card inherits the overridden red theme automatically. The var(--brand-color, #555555) fallback means the button renders gray if the custom property is missing or unsupported.
Reading and writing variables with JavaScript
Custom properties are accessible through the CSS Object Model. Run this in the browser console or in a page script; no special permissions are required beyond ordinary JavaScript execution.
// Read a global variable
const rootStyle = getComputedStyle(document.documentElement);
console.log(rootStyle.getPropertyValue('--brand-color'));
// Write a global variable
document.documentElement.style.setProperty('--brand-color', '#d93025');
// Write a scoped variable on a container
const container = document.querySelector('.card');
container.style.setProperty('--surface-color', '#fce8e6');
Expected check: after setProperty, inspect any element using that variable in DevTools and confirm the Computed tab shows the new color. Risk: updating variables on a high-frequency event such as scroll can force repeated style recalculation. Batch changes or apply the variable to a stable container rather than many individual elements.
Limits and common mistakes
- Custom properties cannot be used inside media query conditions, as @keyframes selector names, or as property names. For example,
@media (--dark-mode: 1)is invalid. - They are live CSS values, not preprocessor macros. Changing
--brand-colorupdates every consumer instantly, which can leak styles if a parent variable was never intended for a child component. - Fallbacks can hide missing definitions.
var(--color, var(--fallback))is valid, but it makes debugging harder when the primary variable was simply forgotten. - Type registration with
@propertyenables animation and syntax checking, but support is not universal; Firefox lacked support as of the available research, so feature-detect before depending on it.
Verify the result
- Open DevTools, inspect an element that uses
var(--brand-color), and open the Computed pane to confirm the resolved value. - Temporarily comment out the
--brand-colordeclaration and confirm the fallback value renders instead. - Run
getComputedStyle(document.documentElement).getPropertyValue('--brand-color')in the console to read the current global value. - Use
@supports (--custom: 0)in CSS to gate progressive-enhancement rules for browsers that do not recognize custom properties.
Rollback
Editing custom properties changes only the rendered stylesheet, not build output or server state. To roll back, remove or comment out the --property-name declarations, or revert the style.setProperty call by restoring the previous value or removing the inline style with style.removeProperty('--brand-color').
When to choose custom properties over preprocessor variables
Use custom properties when values need to change after the page loads—dark mode, user themes, component states, or JavaScript-driven configuration. Use preprocessor variables for values that never change at runtime and when you want to keep the CSS output free of unused variable declarations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.