CSS Custom Properties explained
In programming languages the term “variable” describes a storage location normally associated with an identifier that contains some value. Despite fact that CSS is a markup language, spec creators were very generous recently and gave us a tiny, but very powerful piece of real programming capability. Excitement about native CSS Custom Properties is generally tempered by the incorrect comparison to variables used in preprocessors like Sass or LESS. Don’t be fooled by this misconception! Bare with me for the rest of this article and let’s embrace the power of this new native feature together.
Syntax
When I saw the syntax for a first time I wasn’t a big fan of it. To be honest, not much has changed since. One of the spec creators gave us a fair explanation behind the naming decisions.
If we use $foo for variables, we’ll be unable to use it for future “variable-like” things.
A declaration can be made on any selector and it requires a valid identifier that starts with two dashes. Unlike other CSS properties, variable names are case-sensitive. They follow inheritance and specificity rules like all other ordinary properties. If you need a reminder on how specificity in CSS works, I encourage you to read “CSS Specificity explained” first. The common practice is to declare it on the :root
element to keep it accessible in all child selectors.
:root {
--brand-color: #06c;
}
To reference a variable value use the var()
function.
h1 {
color: var(--brand-color);
}
Custom properties can share values between each other.
:root {
--brand-color: #06c;
--logo-color: var(--brand-color);
}
The new var function allows a fallback value in case the property wasn’t declared beforehand.
p {
font-family: var(--font-stack, Roboto, Helvetica);
}
// Yes, quotation marks are not needed
// More about that here
// https://mathiasbynens.be/notes/unquoted-font-family
Variables cannot be a property name or part of a value. The following examples are not valid.
.foo {
--side: margin-top;
var(--side): 20px;
}
.foo {
--gap: 20;
margin-top: var(--gap)px;
}
// Use calc() instead
.foo {
--gap: 20;
margin-top: calc(var(--gap) * 1px);
}
The difference between native CSS Custom Properties and Sass variables
Preprocessors like Sass or LESS are fantastic! The variables available in these tools are not exactly the same as CSS Custom Properties though. Further, the variables that are available in the popular PostCSS plugin called cssnext use the same syntax as native CSS Custom Properties but still behave exactly the same as those from Sass or LESS. They apply a static value to the declaration during the compilation process. The new native CSS feature applies a value to a DOM element on runtime meaning that the browser allows us to reassign it. The dynamic nature of this feature allows us to take advantage of other language facilities like media queries or cascading. Let’s have a look at the classic example that is impossible with preprocessors, but works fine with native CSS.
$fz: 1rem;
@media (min-width: 60rem) {
$fz: 1.5rem;
}
body {
font-size: $fz;
}
The above example applies a static font-size: 1rem
to the body
tag. The media query is completely ignored because a media query is not something that works in the C or Ruby compiler, it is something that works only in the browser. To make it work in Sass we have to create two separate variables and assign them depending of the breakpoint. CSS Custom Properties don’t require the additional compilation process so the browser applies the value at runtime. When we change the width of the viewport, the value of the property is reassigned as expected.
:root {
--fz: 1rem;
}
@media (min-width: 60rem) {
:root {
--fz: 1.5rem;
}
}
body {
font-size: var(--fz);
}
In my opinion it is freaking awesome and it opens an array of new options that weren’t possible before. I have many more examples in my head, but hopefully this one clearly illustrates the subject.
Working with CSS Custom Properties and JavaScript
The true power of CSS Custom Properties comes when we combine it together with JavaScript. It takes very few lines of code to allow us to get and change the value of CSS custom property. Let’s go through a few simple examples.
Get the value of CSS Custom Properties
To get a value of a property we have to use getPropertyValue()
.
:root {
--brand-color: salmon;
}
var styles = getComputedStyle(document.documentElement);
var customProp = String(styles.getPropertyValue('--brand-color')).trim();
console.log(customProp);
// salmon
Reassign the value of CSS Custom Properties
To reassign the value setProperty()
comes handy.
:root {
--brand-color: salmon;
}
p {
color: var(--brand-color);
}
document.documentElement.style.setProperty('--brand-color', 'purple');
<p>I’m a purple paragraph!</p>
Detect the browser support for CSS Custom Properties
Browser support for CSS Variables isn’t great at the time of writing this article. Google Chrome 49+, Firefox 31+, Safari 9.1+ and iOS 9.3+ doesn’t make it reliable enough to use it in production. Luckily we have a few methods to detect the hero of today’s article. Native feature detection with CSS.supports() API helps us with that. Lets have a look at how to use it in CSS and JavaScript.
body {
--bg-color: #e46764;
background-color: pink;
}
@supports (background-color: var(--bg-color)) {
body {
background-color: var(--bg-color);
}
}
CSS.supports('--bg-color', '#e46764');
// returns a boolean value
This method isn’t bulletproof because support for @support
isn’t amazing at all. Wes Bos posted a much more reliable method the other day. Thanks man!
function testCSSVariables() {
// create a new element
var el = document.createElement('span');
// add a new custom property and apply it as a background
el.style.setProperty('--color', 'rgb(255, 198, 0)');
el.style.setProperty('background', 'var(--color)');
// print new element to the DOM
document.body.appendChild(el);
// assign computed styles of element
var styles = getComputedStyle(el);
// return true if background color is assigned correctlly, false otherwise
var doesSupport = styles.backgroundColor === 'rgb(255, 198, 0)';
// remove element from the DOM
document.body.removeChild(el);
// return boolean value
return doesSupport;
}
testCSSVariables();
// returns a boolean value
Day / night mode switch
Codepen is already full of beautiful projects that use CSS Custom Properties. Mine is nothing near this level of creativity but I had good fun doing it. Before checking example below, please make sure your browser supports CSS Custom Properties first. I’m super curious about how you use CSS Custom Properties. Don’t be shy and post a link in the comments section :)
See the Pen 2016-02-19-css-custom-properties-explained by Pawel Grzybek (@pawelgrzybek) on CodePen.
would be super nice if these variables were controllable with the use of pseudo-classes
etc.
They are.
Cool, U are right!
For some reason it doesn't work in JSFiddle when it is set to .scss
but in .css it works just fine : )
https://jsfiddle.net/z8f3vj...
In "Day / night mode switch" JavaScript could be omitted then : D
Yeah, your example is cool!
Custom properties are scoped to selector, so as long as something is in the scope, it's fine to modify. I used JavaScript on purpose. I wanted to use in one simple example all the techniques that I mentioned on the blog post.
It's strange that it doesn't work with .scss. Sass compiler skips all the var declarations as expected. I'm almost sure it is just a jsfiddle specific behavior, because I can't see any reason why it shouldn't work.
Hi Pawel, thanks for your article! I don't really comment on blogs very often but I just wanted to say I appreciated you saying "Mine is nothing near this level of creativity but I had good fun doing it." - a level of modesty is what I find is sometimes sorely lacking in our industry. Good on you for setting a positive example!
Ha :) Thanks for nice input @asinnema:disqus. Thanks for visiting my website and have a great day!
as per it's abbreviation CSS which stand for central superior services, I have found it superior and fantastic.
Awesome article and great example implementation, love the simplicity and absence of bloat.
Nice trick pushing 50% and translating back.
Here is a slight simplification with fewer transforms and some more variable to control the label width inside calc:
https://codepen.io/dmitriz/...
BTW, you can simplify `if(mode.checked === true)` to `if(mode.checked)`.
Nice nice nice! Thanks for kind words about post. Thanks for hints about simplification. Have a great day.