Update: there was a simplification about Custom Attributes being not supporting concatenation, thanks to Sime Vidas, Brian Kardell and Greg Whitworth to set this straight.
Over the last few iterations of CSS the boundaries between CSS and JavaScript started to blur. CSS was a static language, meant to define colours, look and feel, but not interactivity. You could use percentages in dimensions as sort of interactivity to the enviroment, but reacting to things changing was the job of JavaScript.
In the days of old HTML was there to give structure, CSS look and feel and JavaScript interactivity. Or, as I put it in my book in 2006, if your website were a movie HTML would be the script, CSS the cinematography and direction and JavaScript the special effects.
Nowadays, CSS is much more powerful. We have animations, transitions, calc() and much more flexible values like em, rem, vw, vh and more. We also have interactivity with pseudo selectors like hover, focus and states of interactive elements like buttons. We can even hack with checkboxes to write full games in pure CSS.
This is great! CSS aficionados are much more likely to have the patience and knowledge to make an animation or interaction look and behave exactly right. And CSS engines are responsible to perform well and not clobber the interactivity or battery life of the end user device. Browser makers can concentrate on optimising the engine rather than competing with the developer on who has the job to keep things smooth.
However, there are still boundaries and use cases where CSS is not enough, and you need JavaScript. Often these are about reading the current state of something happening to the browser window or an interaction not considered in the CSS spec.
Switching fully to JavaScript in that case feels like a knee-jerk reaction and it makes more sense to me to find a way for JavaScript and CSS to interact. JavaScript to read the value and to make it available to CSS in some way.
In the past, the only way to do that was to store classes on parent elements or remove classes when certain conditions were met. But with Custom Properties (“CSS variables”), it has become much easier to interact between JavaScript and CSS.
Custom properties allow you to set “variables” in CSS and use them later. For example:
::root {
--company-blue: #369;
}
h1 {
color: var(--company-blue);
}
Custom properties are somehow limited and are not strings like they are in CSS preprocessors, so you can't readily concatenate them Custom properties work differently to CSS variables in preprocessors. You can concatenate them, but there are limitations.
Thanks to Šime Vidas who showed a working demo on Twitter and to Brian Kardell pointing to the discussion on the CSS standards group.
As my colleague Greg Whitworth explains:
This is not actually true regarding custom props. The issue I think you're primarily referring to are potential limitations in CSS in general, although I noticed that Sime already showed you that concatenation can be done but probably not in all scenarios where the only desire is to join actual strings (eg: "bar" calc(5 + 8) will get converted to \"foo\" calc(58) since it isn't a valid calc and thus those are converted to a string as well but with escaped quotes. Everything within the variable is tokenized, so it can be a string or not depending on what the value is determined by the tokenizer. If it isn't an invalid ident but isn't one that can be matched, then it is converted to a string for use within CSS. Anything passed to JS is converted to a string. You can see this in this JSBin
The simplest way to alter CSS custom properties is to use calc() to multiply them with a value:
::root {
--startwidth: 200;
}
h1 {
width: (var(--startwidth) * 1px);
}
h2 {
width: (var(--startwidth) * 0.5px);
}
Now, as you can also define custom properties in JavaScript and add them to the styles collection of any element, this is a great way to only use JavaScript to read a value and leave the rest to CSS.For example, if you wanted to know how far the document has scrolled, you can read this in JavaScript with an event handler and modify a CSS custom attribute:
window.addEventListener('scroll', (e) => {
document.body.style.setProperty('--scrolly', window.scrollY);
});
CSS:
h1 {
position: fixed;
width: calc(var(--scrolly) * 1px);
background: #339;
}
You can try this out in this JSBin
By no means, this is a sensible demo, but I really like the fact that you can use JavaScript to reach where CSS can not, and still allow CSS to be the main driver and definition of interactivity.