Modern Custom Properties with Defaults
Back in 2021, Lea Verou shared a brilliant pattern for handling default values in CSS variables. Now, in 2026, while that pattern remains useful, we also have the powerful @property rule as a standard feature. Let’s look at both the “Classic” and “Modern” ways to handle component defaults.
The “Classic” Way: The --_property Pattern
This technique solves the specificity wars by using a “private” internal variable.
The Problem
If we define a component’s style using a variable with a fallback in multiple places:
.button {
background: var(--bg, black);
border-color: var(--bg, black);
}
We are repeating var(--bg, black). If we want to change the default to blue, we have to edit it everywhere.
The Solution: Pseudo-Private Variables
We create a new variable, prefixed with _ (underscore), which acts as our internal source of truth.
.button {
/* Internal variable = Public variable OR Default */
--_bg: var(--bg, black);
/* Use the internal variable everywhere */
background: var(--_bg);
border: 1px solid var(--_bg);
}
To customize it, the user just sets the public variable:
.button.red {
--bg: red;
}
This acts like a Constructor for your CSS component. It’s lightweight, requires no global registration, and works perfectly for local scope.
The “Modern” Way: @property
With widespread support for @property, we can now define variables that really have defaults, types, and are even animatable.
Instead of needing a “private” intermediate variable, we register the public property directly.
@property --button-bg {
syntax: "<color>";
initial-value: black;
inherits: true;
}
.button {
/* No need for var(--button-bg, black) - the fallback is native! */
background: var(--button-bg);
border: 1px solid var(--button-bg);
/* Plus: We can now transition this variable! */
transition: --button-bg 0.3s;
}
Why use @property?
- Type Safety: The browser knows
--button-bgis a<color>. If someone sets--button-bg: 20px, it is invalid and falls back toblack(the initial value), rather than breaking the UI. - Animation: You can transition from
blacktoredsmoothly because the browser knows how to interpolate colors. Standard variables snap from one value to another. - Cleaner Code: No need for the
--_trick if you are okay with global registration.
Which one to choose?
-
Use the
--_pattern for simple, local components where you don’t want to pollute the global namespace with registered properties or when you need “soft” defaults that change based on context. -
Use
@propertywhen you need animation, strictly enforced types, or are building a robust Design System where properties are well-documented and globally unique.
My Recommendation: The Professional Setup
To take your CSS architecture to the next level, I recommend combining these patterns with a solid foundational setup:
-
Global
:rootVariables: Define your design tokens (colors, spacing units, scale) in a base file scoped to:root. -
Organize with
@layer: I highly recommend using the@layerrule to manage your CSS cascade. Since it is now Baseline Widely available, it provides a robust way to handle context and overrides, especially in platforms or frameworks that inject styles at multiple levels. You can read more in my post: How I Use @layer in CSS. -
Consistent Spacing & Sizing: Use a base
--spacingunit and compute your paddings and margins from it (e.g.,padding: calc(var(--spacing) * 4)). This ensures visual harmony across the entire project. -
Fluid Typography with
clamp(): Don’t use static font sizes. Useclamp()to create fluid typography that scales beautifully between mobile and desktop without needing a dozen media queries.
/* 1. Global Setup with @layer and :root tokens */
@layer tokens, base, components;
@layer tokens {
:root {
--spacing: 0.25rem;
/* 2. Fluid Typography with clamp() */
--font-size-base: clamp(1rem, 1.2vw, 1.125rem);
--color-primary: #3b82f6;
--color-text: #1f2937;
}
}
@layer components {
.card {
/* 3. Pseudo-private variables for component logic */
--_bg: var(--card-bg, #ffffff);
/* 4. Consistent spacing using calc() */
--_padding: calc(var(--spacing) * 6);
background: var(--_bg);
padding: var(--_padding);
font-size: var(--font-size-base);
color: var(--color-text);
border: 1px solid var(--color-primary);
border-radius: calc(var(--spacing) * 2);
/* Enable transition for the custom property */
transition: --card-bg 0.3s ease;
}
}
/* 5. Registered @property for smooth transitions */
@property --card-bg {
syntax: "<color>";
initial-value: #ffffff;
inherits: false;
}
.card:hover {
--card-bg: #f3f4f6;
}
By combining Lea’s --_ pattern for components with a global :root system, you get the best of both worlds: global consistency and local flexibility. In fact, I used this exact architecture to build this very website!
Updated for 2026. Based on Custom properties with defaults: 3+1 strategies




Comments for LVCSTP