CSS Custom Properties are the single greatest addition to CSS over the past 20 years

On Twitter, John Allsopp recently shared that Flexbox is the greatest thing that happed to CSS. I quickly chimed in with my personal favourite. To me the single greatest addition to CSS over the past 20 years is Custom Properties. They’ve opened up a ton of possibilities, and make us CSS Authors way more productive. The way they are specced — as properties and not simply variables — is pure genius and gives them their true power.

~

💁‍♂️ I heard spec author Tab Atkins talk about them for the first time at CSS Day 2013, but it wasn’t until Lea Verou’s talk CSS Variables: var(--subtitle); in 2016 that it really clicked for me.

~

# Why Custom Properties are so great

People often refer to CSS Custom Properties as “CSS Variables”. While they can be used like variables you know from preprocessors like Sass or Less, calling them mere variables sells them short. The true power of Custom Properties lies in the fact that they are actual CSS properties, which means they participate in the cascade.

Unlike preprocessor variables — which are static and get compiled away at build time without any knowledge of the DOM or runtime conditions — Custom Properties are evaluated live in the browser and follow standard CSS cascading and inheritance rules. Therefore, they can:

  1. Inherit from one element onto another (and down the DOM tree).
  2. Change dynamically (in a media query, on a specific subtree, etc.).

Additionally, thanks to CSS Houdini, you can register custom properties with the amazing @property (only in Chrome though), controlling what they should compute to, whether they inherit or not, and what their initial value is.

~

1. They can inherit and compose

Because Custom Properties inherit by default, a custom property declared on a parent element (such as :root) is automatically available to all of its descendants. Furthermore, one custom property can reference and build upon another custom property:

:root {
	--brand-color: hsl(200deg 80% 50%);
	--text-color: #222;
}
.button {
	--button-bg: var(--brand-color);
	--button-fg: white;
	background-color: var(--button-bg);
	color: var(--button-fg);
}

By wiring up component-specific properties to global tokens (or composing values with calc() and color functions), you can create flexible styling APIs right in CSS.

Update 2024.07.01: Do note that Custom Properties compute before they inherit. See the tale of computing cos(25deg) for more details.

~

2. They can change dynamically

Because Custom Properties participate in the cascade, you can override their values anywhere in your CSS — inside a @media query, on a pseudo-class like :hover, or scoped to a specific DOM subtree. When a custom property’s value changes, any rule that references it updates automatically, without you having to redeclare the entire ruleset.

:root {
	--bg-color: #fff;
	--text-color: #111;
	--spacing: 1rem;
}
/* Dynamically adjust based on viewport size */
@media (min-width: 60rem) {
	:root {
		--spacing: 2rem;
	}
}
/* Dynamically adjust for dark mode OR a specific dark subtree */
@media (prefers-color-scheme: dark) {
	:root {
		--bg-color: #111;
		--text-color: #eee;
	}
}
. inverted-section {
	--bg-color: #111;
	--text-color: #eee;
}
section {
	background-color: var(--bg-color);
	color: var(--text-color);
	padding: var(--spacing);
}

Notice how the section selector only needs to be written once. You just flip a few switches in a media query or on a parent container, and the entire subtree responds to it.

~

Registering Custom Properties with @property

By default, standard Custom Properties are untyped and always inherit down the DOM tree. However, using Houdini’s @property at-rule, you can formally register a custom property to give it a specific type (syntax), an initial-value, and control whether it inherits or not:

@property --color {
	syntax: '<color>';
	initial-value: hsl(200deg 15% 73%);
	inherits: false;
}
🎩 Houdini, ain't that a magician?

Houdini is a set of low-level APIs that exposes parts of the CSS engine, giving developers the power to extend CSS by hooking into the styling and layout process of a browser’s rendering engine. Houdini is a group of APIs that give developers direct access to the CSS Object Model (CSSOM), enabling developers to write code the browser can parse as CSS, thereby creating new CSS features without waiting for them to be implemented natively in browsers.

It really is magic, hence it's name Houdini. I'd recommend this slidedeck and this video to get you started

Registering a custom property has several major benefits: the browser can validate its value (falling back to initial-value if assigned an invalid type), you can prevent it from bleeding into child elements by setting inherits: false, and — because the browser now knows the property’s type — you can smoothly transition and animate it!

Update 2023.02.01: While registering a custom property with @property allows you to animate it, keep in mind that animated custom properties run on the main thread. Read The gotcha with animating custom properties for all the details.

~

# Notes on using Custom Properties

While Custom Properties are fantastic, there are a few nuances, patterns, and features worth keeping in mind when authoring CSS with them.

~

Don’t overly rely on Custom Properties

Over at CSS-Tricks, Chris Coyier wrote about the Different Degrees of Custom Property Usage: how do you provide defaults, and how far do you go when splitting things up into several parts?

Regarding the fallback part, Chris sticks to what we already know: a second argument to var():

el {
	border: .1em solid var(--color, hsl(200deg 15% 73%));
}

The interesting part is on splitting things up: do you provide one single hsl() color, or do you split out all the individual channels into separate custom properties?

el {
	--color: hsl(200deg 15% 73%);
	border: .1em solid var(--color);
}
el {
	--color-h: 200deg;
	--color-s: 15%;
	--color-l: 73%;
	--color-hsl: var(--color-h) var(--color-s) var(--color-l);
	--color: hsl(var(--color-hsl));
	border: .1em solid var(--color);
}

Just like Chris, I lean more towards using a single --color here because although Custom Properties are very nice, overusing them comes at a cost: complexity. As I tweeted back in February:

Thankfully there’s DevTools’s “Computed Styles” pane to somewhat help me out there 🙂

Additionally, setting those individual channel properties on :root and then overriding --color-h on a specific child element won’t work as you might expect, because --color will already have been computed on :root before it is passed down the inheritance tree. This is what Chris calls The Big Gotcha With Custom Properties.

~

Pseudo-private Custom Properties

Over at her blog, Lea Verou also wrote about Custom Properties and how to provide defaults. Rather than directly using the passed-in custom property with its fallback repeated everywhere, Lea resorts to a pattern she calls “pseudo-private custom properties” that get an extra _ prefix:

My preferred solution is what I call pseudo-private custom properties. You use a different property internally than the one you expose, which is set to the one you expose plus the fallback.

Like so:

el {
	--_color: var(--color, hsl(200deg 15% 73%));
	border: .1em solid var(--_color);
}

That way, consumers of your component can set --color from the outside, while internally your component only has to reference var(--_color) without repeating the default fallback value across multiple declarations.

~

👀 Don’t try this at home

Because the contents of (unregistered!) Custom Properties are not parsed until they are used (using var()), you can store anything in them. That means you could abuse them to create faux Single-line Comments in CSS:

But you should really not do that, as now that property by default inherits onto every child of that element

~

🔥 Like what you see? Want to stay in the loop? Here's how:

I can also be found on 𝕏 Twitter and 🐘 Mastodon but only post there sporadically.

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Join the Conversation

2 Comments

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.