Lea Verou @leaverou
- CSS WG Invited Expert since 2012
- Co-editor of 5 CSS specifications
- Elected W3C TAG Member
- Day job: Usable Programming @ MIT (research & teaching)
- Started dozens of open source projects, some of them used on millions of websites
Slides for Frontend Masters Course “Dynamic CSS”
accent-color: #f06;
heading-font: Fancy Webfont, serif;
h2 {
color: accent-color;
font-family: heading-font;
}
:root {
--accent-color: #f06;
--heading-font: Fancy Webfont, serif;
}
h2 {
color: var(--accent-color);
font-family: var(--heading-font);
}
{…}
blocks,
CSS variables are scoped on elements
I thought we weren’t supposed to call them CSS variables?
:root
= the root element. <html>
in HTML, <svg>
in SVG etc:root
= html
but with higher specificity
/* This is fine */
:root {
--accent-color: #f06;
--font-body: FancyFont, serif;
}
html {
background: yellow;
min-height: 100vh;
}
/* This is also fine */
html {
--accent-color: #f06;
--font-body: FancyFont, serif;
background: yellow;
min-height: 100vh;
}
/* This is also fine */
:root {
--accent-color: #f06;
--font-body: FancyFont, serif;
background: yellow;
min-height: 100vh;
}
Valid Sass code | Invalid CSS code | |
---|---|---|
Property names |
|
|
Selectors |
|
|
@rules |
|
|
If color-scheme: light dark
is specified,
canvas
and canvastext
act like this:
:root {
--canvas: white;
--canvastext: black;
}
@media (prefers-color-scheme: dark) {
:root {
--canvas: black;
--canvastext: white;
}
}
CSS.registerProperty({
name: "--corner-size",
syntax: "*";
inherits: false
})
@property | 85 | 85 | ||
---|---|---|---|---|
CSS.registerProperty() | 79 | 79 |
background: red;
background: var(--accent-color, orange);
CSS Variables | 49 | 31 | 15 | 9.1 |
---|
CSS Variables | 49 | 31 | 15 | 9.1 |
---|---|---|---|---|
@supports | 28 | 22 | 13 | 9 |
@property
then?
if (window.CSSPropertyRule) {
let root = document.documentElement;
root.classList.add("supports-atproperty");
}
background: var(--nonexistent, none, yellowgreen);
/* Resolves to: */
background: none, yellowgreen;
--color-initial: black;
...
color: var(--color, var(--color-initial));
--__color: var(--color, black);
...
color: var(--__color);
@property --color {
syntax: "<color>";
initial-value: black;
inherits: true;
}
...
color: var(--color);
background: red;
background: var(--accent-color, orange);
background: red;
background: var(--accent-color, 42deg);
background: gold;
background: lch(60% 100 0);
--color: lch(60% 100 0);
background: gold;
background: var(--color);
--color: gold;
--color: lch(60% 100 0);
background: var(--color);
LCH:
LCH:
LCH:
No LCH:
No LCH:
No LCH:
font-size: 20px;
font-size: clamp(16px, 100 * var(--font-size-scale), 24px);
What happens in browsers that don’t support clamp()?
Valid Sass code | Invalid CSS code |
---|---|
|
|
Sass | Compiled CSS code |
---|---|
|
|
background: red;
background: var(--accent-color, orange);
background: red;
background: linear-gradient(white, transparent)
var(--accent-color, orange);
Base CSS uses var()
and sets defaults, MQs override
margin: var(--gutter);
padding: calc(.6em + var(--gutter) * 2);
Base CSS sets different custom properties for each breakpoint, MQs use them
font-size: 90%;
--font-size-large: 110%;
Base CSS multiplies by optional scaling factor, MQs set said factor
font-size: calc(90% * var(--font-size-scale, 1));
--color-red: var(--color-primary);
All of the above can coexist!
[CSS variables] can even be transitioned or animated, but since the UA has no way to interpret their contents, they always use the "flips at 50%" behavior that is used for any other pair of values that can’t be intelligently interpolated.
CSS Custom Properties for Cascading Variables Module Level 1
none
no-repeat
42.1
12px
360deg
5%
,
/
rgb(
image-set(
)
#ff0066
#container
"YOLO"
url(kitten.jpg)
--type: linear-gradient(;
background: var(--type) white, black );
--stops: white, black,;
background: linear-gradient( var(--stops) red);
--to: to;
background: linear-gradient( var(--to) right, white, black );
Number → unit: | calc(var(--foo) * 1px) |
Unit → number: |
--if-not-foo: calc(1 - var(--if-foo));
property: calc(
var(--if-foo) * value_if_true +
var(--if-not-foo) * value_if_false
);
property: calc(
var(--p) * min +
calc(1 - var(--p)) * max
);
/* Round: */
--integer: calc(var(--number));
/* Floor: */
--integer: calc(var(--number) - 0.5);
/* Ceil: */
--integer: calc(var(--number) + 0.5);
Credit to [Ana Tudor](https://twitter.com/anatudor/status/1399849494628425734) for discovering this trick.
|
CSS limitation |
|
CSS bug |
|
Works! |
/* a.css */
--img: url("a.png");
/* b.css */
background-image: var(--img);
// Get variable from inline style
element.style.getPropertyValue("--foo");
// Get variable from wherever
getComputedStyle(element).getPropertyValue("--foo");
// Set variable on inline style
element.style.setProperty("--foo", 38 + 4);
let root = document.documentElement;
document.addEventListener("pointermove", evt => {
let x = evt.clientX / innerWidth;
let y = evt.clientY / innerHeight;
root.style.setProperty("--mouse-x", x);
root.style.setProperty("--mouse-y", y);
});
0-1 can be converted to a length:
calc(var(--mouse-x) * 100vw)
…but the reverse isn’t possible
let rect = evt.target.getBoundingClientRect();
let top = evt.clientY - rect.top;
let left = evt.clientX - rect.left;
let x = left / rect.width;
let y = top / rect.height;
evt.target.style.setProperty("--mouse-local-x", x);
evt.target.style.setProperty("--mouse-local-y", y);
for (let input of document.querySelectorAll("input")) {
input.style.setProperty("--value", input.value);
}
document.addEventListener("input", evt => {
let input = evt.target;
input.style.setProperty("--value", input.value);
});
for (let element of document.querySelectorAll(".typing")) {
let length = element.textContent.length;
element.style.setProperty("--length", length);
}
document.addEventListener("scroll", evt => {
let el = evt.target;
let maxScroll = el.scrollHeight - el.offsetHeight;
let scroll = el.scrollTop / maxScroll;
el.style.setProperty("--scroll", scroll);
}, {capture: true});