Notes for a talk on the topic.
- Fallback
- Variable stacks
- Inheritance & proximity
initial
keyword- Component variants
- Theming & dark mode
- DRY & the logic fold
- Taming source order
- JavaScript & Houdini
CSS Custom Properties are property names prefixed with --
containing a value that can be used in other declarations using the var()
function.
Like preprocessor variables (Sass, PostCSS):
$colour-primary: #fdc605;
.primary-thing {
background-color: $colour-primary;
}
We can define global variables for reuse:
:root {
--colour-primary: #fdc605;
}
.primary-thing {
background-color: var(--colour-primary);
}
Custom properties are scoped to the elements they are declared on, and participate in the cascade, so normal CSS behaviour such as specificity applies.
Codepen scope and cascade example.
The var()
function accepts a fallback value:
.thing {
color: var(--thing-text, black);
}
But it only accepts a single fallback - a limitation is required to support fallbacks with a comma inside them, such as background-image
or font stacks.
We can workaround this by nesting another var
as the fallback, allowing us to create 'variable stacks`.
/* link.css */
border-color: var(--link-hover, var(--link-text, var(--colour-assistant)));
We've done this in the Nucleus branding API - more details below.
In the absense of a 'local' custom property, the var()
function will inherit the value from the property in closest proximity.
This allows for using custom properties other than where they are defined.
Setting a custom property value to initial
will make it fallback to the next available value.
If a value is invalid for a given property, such as 20px
for color
, this sets the entire property value to initial
.
Using the inheritance behaviour, we can expose sets of CSS Custom Properties to create simple APIs for component variants.
Custom properties enable theming independent of CSS structure. We can make use of the fallback values to set defaults and expose named custom properties for specific overrides.
This is the basis of the Nucleus branding API. Each branded component implements one of the three base palette colours as a default: primary, accent or assistant. These colours can then be overridden where necessary.
/* button.css */
border-color: var(--button-fill, var(--colour-accent));
/* link.css */
color: var(--link-text, var(--colour-assistant));
To set up a new theme, an application simply needs to define the palette of three colours:
:root {
--colour-primary: #fdc605;
--colour-accent: #fdc605;
--colour-assistant: #0058cc;
}
For backgrounds, borders and utility text, we similarly defined a palette of reversible greys which can be redefined for a dark context
/* light mode */
:root {
--tint-1: hsl(0, 0%, 20%);
--tint-2: hsl(0, 0%, 40%);
--tint-3: hsl(0, 0%, 60%);
--tint-4: hsl(0, 0%, 80%);
--tint-5: hsl(0, 0%, 90%);
--tint-6: hsl(0, 0%, 95%);
}
/* dark mode */
@media (prefers-color-scheme: dark) {
:root {
--colour-assistant: #4d9aff;
--tint-2: hsl(0, 0%, 60%);
--tint-6: hsl(0, 0%, 10%);
}
}
/* or */
.dark-section {
--colour-assistant: #4d9aff;
--tint-2: hsl(0, 0%, 60%);
--tint-6: hsl(0, 0%, 10%);
}
This guy called Mike has coined a term for an interesting concept based on using CSS Custom Properties: the logic fold.
The idea here is that the relevant variable values are positioned at the top of the file where it can be made easier to trace how a custom property changes. Then "below the fold" is the declarative CSS which implements these variables.
Similar to using preprocessed variables, custom properties allow for removing repetition resulting in more readable code.
CodePen example - grid columns.
When using an authoring system such as CSS Modules in conjunction with Webpack, one does not have control over the final source order of the compiled CSS file. This is determined by the order in which imports are resolved during the build process.
While the encapsulation afforded by CSS Modules is greatly beneficial, one annoying side effect is that two selectors of equal specificity may be compiled in a different order, with application styles coming before library styles. The hacky workaround to ensure the correct styles are applied is to chain the selector to itself to 'win' by specificity.
Custom properties allow us to avoid needing to reach for this hack.
CodePen example - taming source order
Something like a linear-gradient
can have some pretty hostile syntax. This can be greatly simplified with custom properties.
element.style.setProperty("--variable-name")
Example
Credit to Lea Verou for this code.
var root = document.documentElement;
document.addEventListener("mousemove", evt => {
let x = evt.clientX / innerWidth;
let y = evt.clientY / innerHeight;
root.style.setProperty("--mouse-x", x);
root.style.setProperty("--mouse-y", y);
});
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.
In short, Houdini gives a developer building blocks in JavaScript to allow writing new kinds of CSS
.bg {
--square-odd: skyblue;
--square-even: white;
background-image: paint(checkerboard);
}
CodePen example - CSS Paint API
- CodePen collection from this talk
- A Strategy Guide To CSS Custom Properties - Mike Riethmuller, May 2018
- A user’s guide to CSS variables - Lea Verou, May 2020
- Using Custom Property “Stacks” to Tame the Cascade - Miriam Suzanne, June 2020
- Patterns for Practical CSS Custom Properties Use - Tyler Childs, Oct 2019
- CSS Custom Properties In The Cascade - Miriam Suzanne, July 2019
- CSS Variables Secrets (talk) - Lea Verou, 2017
- Houdini: Maybe The Most Exciting Development In CSS You’ve Never Heard Of - Philip Walton, March 2016
- Global and Component Style Settings with CSS Variables - Sara Soueidan, June 2020