Dynamic CSS

By Lea Verou (@LeaVerou)

Hi, I’m Lea! 👋🏼 @leaverou

Web Almanac

HTTP Archive’s annual
state of the web report


Custom properties in 2021

		:root {
			/* depth = 0 */
			--h: 335;

			/* depth = 1 */
			--hs: var(--h) 90%;

			/* depth = 2 */
			--color: hsl(var(--hs) 50%);
Source: [Web Almanac 2021](https://almanac.httparchive.org/en/2021/css#custom-properties)

Custom properties in 2021

Element Usage percentage
:root 60
body 5
Other elements 35

We got kickass reactive variables
and we are still treating them
as glorified constants

Custom properties [reached full browser support 5 years ago](https://caniuse.com/css-variables). Why are we still merely scratching the surface?

Today’s talk:
A story in four acts

The Outlined Button

This is a simplified flat button whose text color becomes its background color on hover, a common effect. Note the repetition required to specify a color variation. Let's use CSS variables to eliminate that! After rewriting with CSS variables, only a single `--color` declaration is sufficient to create a color variation of the entire component. However, the fallbacks are getting quite repetitive. Although the syntax allows us to use a different fallback in every usage, in most cases, we don't need that. Repeating the fallback value over and over is not [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). What can we do to reduce that duplication? Another benefit of custom properties is *encapsulation*. We can change how styling works completely, and as long as we use the same custom properties anyone using and styling our component doesn't need to change a thing.

CSS variables are just regular properties

What does that mean? - They are scoped on the element itself, not on curly braces (lexical vs dynamic scoping) - Can be set via inline styles or even JS setting these inline styles - As we will see later on, they are actually *inherited properties*

Pseudo-private variables succinctly reduce fallback duplication

Pseudo-private variables are a convention: prefix custom properties by `_` to indicate they are for internal use. Useful for baking in fallback values, but also other things.

CSS Variables allow us to create a higher level styling API

Provide encapsulation by exposing a higher level styling API. We can change the underlying CSS and the API we've exposed will continue to work. Theming becomes independent of CSS structure.

CSS variables as API ➡️

CSS variables cannot be interpolated
…unless we register them with @property
(or CSS.registerProperty())

You may also remember a `CSS.registerProperty()` JS API. This came earlier, but at this point support for `@property` is nearly identical. The main reason to use `CSS.registerProperty()` these days is because of its richer error reporting.

`@property`-defined initial values take precedence over `var()` fallback

And if you provide one, it will never get used.

The Glossy Button

CSS variables as API? ➡️

Future: Style container queries

			@container style(--glossy: on) {
				button {
					box-shadow: 0 .1em hsl(0 0% 100% / .4) inset;
					/* ... */

Space Toggle

Invalid At Computed Value Time (IACVT)

			--foo: 42deg;
			background: var(--foo); background: unset;

			background: var(--foo); background: unset;

			--foo: initial;
			background: var(--foo); background: unset;

			background: linear-gradient(white, var(--foo));
background: unset;

			background: var(--foo, 42deg); 
background: unset;

			--foo: ;
			background: var(--foo); background: unset;

			--foo: var(--bar); --foo: unset;
			--bar: var(--foo); --bar: unset;

			--foo: calc(var(--foo) + 1); --foo: unset;

Space Toggle for numerical values

			/* Fictional (future?) syntax: */
			line-height: if(--glossy = initial, 1.1, 1.5);

			/* Space toggle version: */
			line-height: calc(1.5 var(--glossy, - .4));

Space toggle in production


					:root {
						--ON: initial;
						--OFF: ;

					button.primary {
						--glossy: var(--ON);

Space Toggle for progressive enhancement

			:root { --in-oklab: ; }

			@supports (
				linear-gradient(in oklab, red, tan)
			) {
				:root { --in-oklab: in oklab,; }

			/* Usage: */
			background: linear-gradient(var(--in-oklab) #f00, #0f0);

Extreme Space Toggle

Made by [Jane Ori](https://twitter.com/Jane0ri)

The Bar Chart

Number → unit: calc(var(--foo) * 1%)
Unit → number:
Future: calc(var(--foo) / 1%)

Use variables for pure data, not CSS values

This is a good practice regardless of the conversion issues, as it ensures better separation of concerns.

`content` only accepts strings. You can convert numbers to strings via `counter-reset`.

For the same reason, `counter()` returns a string, and thus cannot be used in calculations.

`content` only accepts strings. You can convert numbers integers to strings via `counter-reset`.

For the same reason, `counter()` returns a string, and thus cannot be used in calculations.

The counter trick only works with integers. Sorry!

But what if we convert the number to an integer?

Convert a number to an integer by assigning it to a property registered as `"<integer>"` inside `calc()`

			/* 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 Values 4](https://www.w3.org/TR/css-values-4/) includes a [`round()` function](https://www.w3.org/TR/css-values-4/#round-func) for this, that can also do lengths etc and rounds by arbitrary steps, but it is not currently implemented anywhere.


			/* Round: */
			counter-reset: p round(var(--p), 1);

			/* Floor: */
			counter-reset: p round(down, var(--p), 1);

			/* Ceil: */
			counter-reset: p round(up, var(--p), 1);

Farther future:

			content: text(var(--p)) "%";
hsl(50 100% 50%)
hsl( 100% ­)
hsl(190 100% 40%)


Linear mapping

--x ∈ [x1, x2 ] ⇒ --y ∈ [y1, y2]

			--y: calc(( ( (var(--x) - x1) * (y2 - y1) ) / (x2 - x1) ) + y1);

CSS Variable Conditionals

			--is-not-foo: calc(1 - var(--is-foo));
			property: calc(
				var(--is-foo)     * value_if_true +
				var(--is-not-foo) * value_if_false
You can use linear mapping to do conditionals in a more readable way than space toggle. Essentially you're just mapping the 0 to 1 range and ignoring all values that are not 0 or 1.

Linear mapping for conditionals

			/* Fictional (future?) syntax: */
			line-height: if(--glossy = 1, 1.1, 1.5);

			/* Space toggle version: */
			line-height: calc(1.5 var(--glossy, - .4));

			/* Linear mapping version: */
			line-height: calc(
				var(--glossy)       * 1.1 +
				(1 - var(--glossy)) * 1.5

Logical operations

--not-a: calc(1 - var(--a));
--a-and-b: calc(var(--a) * var(--b));
--a-or-b: min(var(--a) + var(--b), 1);
Interested in finding out more about ways to work around conditionals in CSS? Here are a few good articles: - [DRY Switching with CSS Variables: The Difference of One Declaration](https://css-tricks.com/dry-switching-with-css-variables-the-difference-of-one-declaration/) by Ana Tudor - [DRY State Switching With CSS Variables: Fallbacks and Invalid Values](https://css-tricks.com/dry-state-switching-with-css-variables-fallbacks-and-invalid-values/) by Ana Tudor - [Logical Operations with CSS Variables](https://css-tricks.com/logical-operations-with-css-variables/) by Ana Tudor - [CSS Switch-Case Conditions](https://css-tricks.com/css-switch-case-conditions/) by Yair Even Or

Linear mapping allows the same variable to control multiple things, continously or discretely

Useful for exposing a nice API or simply for minimizing the number of properties you need to register
Using variables for data also helps make code more readable. It makes much more sense to change a `--look` variable from `0` to `1` than from `25px` to `75px`, and affords more flexibility for redesigns. However, this means we need to map that number to the value we need at the point of usage. How can we do that?

Range mapping allows you to decouple input from output

The Speech Bubble

Here we have a child element (our generated content) that needs to size its offset based on the parent font size. However, just using the `em` unit doesn’t help: it refers to its own font size. In this case we know how to reset it, but this solution is fragile. In other cases we can't do this at all, e.g. if the current font size is set to `0`. Can we use a variable set to `1em` on the parent? Let's see what happens. The computed value of unregistered properties is the specified value with any variables substituted. This means that any relative units, such as `em` are resolved at the point of usage, not the point of definition. Registering the property changes this, which means you can use it to grab relative values on any ancestor! [Here is the solution](https://codepen.io/leaverou/pen/XWMZewB?editors=1100) to this exercise. Note that this approach doesn't work with `currentColor`, which is resolved at used value time.

Relative values inherit as syntax tokens *unless* the property is registered

Registering a property as `<length>` allows us to pass down ancestor font sizes!

Chrome Firefox Edge Safari
CSS Variables 49 31 15 9.1
@property 85 85
CSS.registerProperty() 79 79

Detect @property support

			if (window.CSSPropertyRule) {
				let root = document.documentElement;


			.supports-atproperty optgroup {
				font-size: 0;

Can we detect @property with CSS alone?

			@supports ( ??? ) {
				optgroup {
					font-size: 0;

The syntax of registered properties is only checked at computed value time.

From the [spec](https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-custom-properties): When parsing a page’s CSS, UAs commonly make a number of optimizations to help with both speed and memory. One of those optimizations is that they only store the properties that will actually have an effect; they throw away invalid properties, and if you write the same property multiple times in a single declaration block, all but the last valid one will be thrown away. (This is an important part of CSS’s error-recovery and forward-compatibility behavior.) This works fine if the syntax of a property never changes over the lifetime of a page. If a custom property is registered, however, it can change its syntax, so that a property that was previously invalid suddenly becomes valid. The only ways to handle this are to either store every declaration, even those that were initially invalid (increasing the memory cost of pages), or to re-parse the entire page’s CSS with the new syntax rules (increasing the processing cost of registering a custom property). Neither of these are very desirable. Further, UA-defined properties have their syntax determined by the version of the UA the user is viewing the page with; this is out of the page author’s control, which is the entire reason for CSS’s error-recovery behavior and the practice of writing multiple declarations for varying levels of support. A custom property, on the other hand, has its syntax controlled by the page author, according to whatever stylesheet or script they’ve included in the page; there’s no unpredictability to be managed. Throwing away syntax-violating custom properties would thus only be, at best, a convenience for the page author, not a necessity like for UA-defined properties.


Is it all a steaming pile of shit hacks?

API simplicity > Implementation simplicity

Yes, a lot of these are hacks. However, that doesn't mean they are always a bad idea. It is often preferable to trade higher implementation complexity to gain lower API complexity. Implementation can always change; it is much harder to change an API. Overly complex APIs cause a lot more pain than overly complex implementations. So do what you gotta do to expose a nice API. Write components whose CSS properties are independent and each control one conceptual thing. Write components that can work with whatever CSS you throw at them. Write components whose CSS properties can be modified at any point without breakage. And if you need to employ hacks to do these things, so be it. Note that this is not a license to write crappy code. This is about the many cases where you need to make a tradeoff between the two.