CSS custom properties are very well supported and are absolutely something to make a priority to learn and use in 2022.
There are many things to love about custom properties, and we'll overview the following:
- What are CSS custom properties
- How they're different from preprocessor variables
- Why you should use custom properties
- Using them to set defaults
- Custom properties and inheritance
- Accessing and setting custom properties with JavaScript
What are CSS custom properties?
#Custom properties allow you to define variables for re-use in your styles. Just like other CSS properties, they abide by the rules of the cascade. This is a great feature because it means we can define them with defaults and then provide overrides as needed. If you're a JS dev, think of them as equivalent to being defined with let
.
To define a custom property, use the name of your choice preceded by two dashes, for example: --my-property
. Then you can use that value by calling it within the var()
CSS function in place of providing a standard value.
For this class, we're defining a custom --color
property and then using it for the color
value:
.btn {
--color: blue;
color: var(--color);
}
How custom properties differ from preprocessor variables
#Preprocessors like Sass or LESS have had variables for many years. However, those variables are ultimately static once compiled into your final stylesheet. This is great for values that you repeat across your styles but don't have a need to change post-compilation.
Custom properties have the advantage for values that may change dynamically client-side or contextually via inline style overrides. Additionally, using preprocessor variables to set a value such as our button color requires redefining all properties used in a modifier class to change to a different preprocessor variable. In other words, you can't change the value of a preprocessor variable after it was defined, but you can do that with CSS custom properties (pending cascade inheritance).
Another advantage for preprocessor variables over custom properties is defining strings to use anywhere, such as for media query breakpoints, or to even define a selector string which can look like the following:
// Example in Sass which allows overriding the $classes value
$classes: ".class-a, .class-b" !default;
#{$classes} {
// define properties for this rule
}
Why should I use CSS custom properties?
#Let's answer that by enhancing our previous code example for changing the .btn
color.
If I want to change the color of the button text and border on hover, I no longer need to repeat those properties. Instead, we can just change the --color
value.
Notice how we're able to use the var()
function as only part of the value, in this case, to define the border-color
. This opens up endless opportunities for custom properties!
CSS to update color variable for hover state
.btn {
--color: blue;
color: var(--color);
border: 2px solid var(--color);
padding: 0.25em 0.5em;
border-radius: 0.25em;
text-decoration: none;
}
.btn:hover {
--color: red;
}
Using custom properties to set defaults
#A feature of var()
is the ability to both call a custom property, but also set a fallback which you can also think of as a default.
var(--color, blue)
We can even set the default using another custom property. Using that idea, let's create a different version of our .btn
class to allow modifying the --border-color
while also setting the --color
variable as a default.
Combining local variables with defaults
.btn-v2 {
--color: blue;
color: var(--color);
border: 2px solid var(--border-color, var(--color));
padding: 0.25em 0.5em;
border-radius: 0.25em;
text-decoration: none;
}
.btn-v2:hover {
--border-color: red;
}
Custom properties and inheritance
#It's important to learn a few things about how CSS custom properties are computed and inherited.
My own introduction into custom properties led me to believe that I should load them all on the :root
selector. Instead, it's recommended to only hold truly global properties in the :root
, and more class or component-specific styles closer to where they are needed.
A key concept is that custom property values are computed once per element, and then the computed value is available for inheritance. When you use custom properties within calc()
or other values that need to be calculated, like the hue of hsl()
, then you are making the total computed value inheritable. You are not able to change a value within a calculation if it is set on an ancestor, as shown:
:root {
--unit: 10px;
--size-lg: calc(3 * var(--unit));
}
/* this will not use the updated unit value for the calculation */
.margin-top-3xl {
--unit: 30px;
margin-top: var(--size-lg);
}
One option to fix the previous inheritance issue is by using a combination of a base and modifier class so that the values are computed against the same element. In the demo, we're still setting a global --unit
value and using it as the default on the base class.
Handling custom property inheritance
:root {
--unit: 10px;
}
.margin-top {
--margin-unit: var(--unit);
--multiplier: 1;
margin-top: calc(var(--multiplier) * var(--margin-unit));
}
.margin-top--3xl {
--margin-unit: 30px;
--multiplier: 3;
}
div[class*="margin"] {
background-color: dodgerblue;
color: white;
padding: 0.5rem;
font-size: 1.5rem;
}
There are also times where it can be beneficial to use an undefined custom property, such as making utility classes even more flexible. The benefit of an undefined property is that it can inherit its value from any ancestor, which is excellent for updating values for a group of related elements.
Two options for using unset properties:
- Completely undefined with no default, which means it will likely have the behavior of
unset
for that property, for example:color: var(--color)
where--color
is not set within the rule - Undefined with a fallback to get the benefits of both inheritance and ensuring a default, example:
color: var(--color, blue)
We used the second option in the demo around setting defaults to allow an optional --border-color
value.
A gotcha with the first is that you may expect it to use a previously set style for that element in your stylesheet. However, by the time the custom property is evaluated the browser will have already tossed out a previously set, non-inherited value.
In this demo, if --color
is not set, all paragraphs will use unset
behavior. For paragraphs, this evaluates to using inherit
for color
. Since the custom property caused throwing out the previously set paragraph style, the color will inherit from the nearest ancestor instead. You can experiment with adding --color
to the paragraph using dev tools to see it successfully set and used. Adding via dev tools works due to causing a repaint.
Demonstration of custom properties using 'unset' behavior
/* tossed out */
p.unset { color: red }
/* behaves as 'inherit' per 'unset' rules */
p.unset { color: var(--color) }
My color will inherit from the nearest ancestor where it is set
Accessing and setting custom properties with JavaScript
#There are typically JavaScript counterparts for interacting with CSS, and using JS to dynamically set or update custom properties is kind of a superpower. We can keep the overall style setup in our CSS and update parts of it as needed.
Some opportunities to update custom properties with JavaScript:
- CSS transform values for animations
- values within a calc to use viewport dimensions
- any number value that can't be gained in CSS alone
- filling the gap for unsupported CSS properties
To access the value of a custom property within JavaScript, use the following combination of getComputedStyle()
with getPropertyValue()
.
getComputedStyle(element).getPropertyValue("--my-var");
In this next demo, we're using JavaScript to get the width of the paragraph. Then we assign that to the custom property assigned as the hue value within the hsl()
color function. As an aside, this works because hsl
allows any numeric value for hue because it just loops back around the color wheel for numbers greater than 360.
Updating custom properties with JavaScript
.js-color {
background-color: hsl(var(--hue), 100%, 80%);
color: black;
font-size: 1.5rem;
padding: 1rem;
}
.js-color::after {
content: " Current: " attr(style);
}
const p = document.querySelector('.js-color');
const setColor = () => p.style.setProperty('--hue', p.offsetWidth);
window.onresize = setColor;
setColor();
Resize your viewport to change my background color (warning: flashing possible).
Combining preprocessor variables and CSS custom properties
#As a software engineer who works on design systems, I've grown fond of using Sass together with custom variables so that I can use the strengths of each.
In order to use a Sass variable as a custom property value, you need to use interpolation which looks like this:
--custom-property: #{$sass-var};
In my work, I'm usually including theming capability. With Sass, we can set static variables using the !default
flag which means they are overridable. Then, we can pass that in to be used as custom property values. Plus we can take advantage of other Sass features like looping to help produce similar groups of properties.
$color-link: blue !default;
$font-sizes: (
"small": .875rem,
"normal": 1rem,
"medium": 1.25rem,
"large": 2rem
) !default;
:root {
--color-link: #{$color-link};
@each $size, $value in $font-sizes {
--font-size-#{$size}: #{$value};
}
}
This setup allows re-theming an application without modifying the core Sass file setup.
Summary of ways to use custom properties
#- color theme values and other design tokens
- to update select values across states, as we did for the
.btn
demo - allow contextual updates for classes while providing defaults
- in partnership with values received from JS
- to compensate for preprocessor values being unable to change after being set
- ...and a whole lot more
Additional resources
#- Posts from Lea Verou
- A strategy guide to structuring and applying custom properties
- More info on how to work with custom properties and the cascade
- The CSS-Tricks complete guide
- 7 practical ideas for using custom properties from Michelle Barker on CSS-IRL
- Also hear more about custom properties from Michelle in this recording from Smashing Meets
- Even more practical use cases from Ahmad Shadeed