Dynamic CSS
Secrets

By Lea Verou (@LeaVerou)

Hi, I’m Lea! 👋🏼 @leaverou

Web Almanac

HTTP Archive’s annual
state of the web report

almanac.httparchive.org

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 three 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` is global
(except in Shadow trees)

And if you provide one, it will never get used.
Chrome Firefox Edge Safari
CSS Variables 49 31 15 9.1
@property 85 85
CSS.registerProperty() 79 79

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;
					/* ... */
				}
			}
		

stateofcss.com Make your voice heard!

Want browsers to prioritize implementing that and other things you care about? Make sure to fill the State of CSS survey — browsers are funding it and looking at its results to decide what to work on. Spending a few minutes to fill it out thoughtfully could save you hours of work down the line.

Space Toggle for numerical values


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

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

Fallbacks without variables


			background: hsl(177.3 100% 30.66%);
			background: lch(60% 60 190);
		
To fully understand how this works, we need to understand the concept of Invalid At Computed Value Time (IACVT). In pre-variables CSS, When the parser encounters a value it doesn't understand, it can just throw it away. This is how we usually get fallbacks: we write two declarations, one with the newer value, and one with older CSS, older browsers ignore the newer CSS, newer browsers override the first declaration with the second.

Fallbacks with variables?


			--color-fallback: hsl(177.3 100% 30.66%);
			--color: lch(60% 60 190);
			background: var(--color-fallback);
			background: var(--color); background: unset;
		
Can we do the same thing with variables? The thing is, variables are resolved later, at computed value time. Until then, the browser needs to assume any declaration that includes a variable anywhere could potentially be valid. So, here, the first background declaration would just be thrown away.

Invalid At Computed Value Time (IACVT)


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

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

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

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

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

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

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

			background: var(--foo, 42deg); 
background: unset;
IACVT comes into play not just when the browser doesn't understand a property value after substitution, but also when the property value is blank, when one or more variable values are missing, or when there are cycles. Also, the fallback can make a declaration IACVT as well.

Fallbacks with variables


			--color: hsl(177.3 100% 30.66%);

			@supports (background: lch(0% 0 0 )) {
				--color: lch(60% 60 190);
			}

			background: var(--color);
		
Can we do the same thing with variables? The thing is, variables are resolved later, at computed value time. Until then, the browser needs to assume any declaration that includes a variable anywhere could potentially be valid. So, here, the first background declaration would just be thrown away.

Space toggle in production

design-system.css

					:root {
						--ON: initial;
						--OFF: ;
					}
				
website.css

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

Gradient interpolation in color space


			.foo {
				background: linear-gradient(#f00, #0f0);
			}

			@supports (background: linear-gradient(in lab, red, tan)) {
				.foo {
					background: linear-gradient(in lab, #f00, #0f0);
				}
			}
		

Space Toggle for progressive enhancement


			:root {
				--in-lab: ;
			}

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

			.foo {
				background: linear-gradient(var(--in-lab) #f00, #0f0);
			}
		
[Tweet](https://twitter.com/LeaVerou/status/1532035426805878784)

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.

Future:


			/* 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);
		

Color interpolation


				/* Now: */
				--h: calc(50 + (190 - 50) * var(--p) / 100); /* 50 to 190 */
				--l: calc(50% + (40% - 50%) * var(--p) / 100); /* 50% to 40% */
				background: hsl(var(--h) 100% var(--l));
			

				/* Near future: */
				background: color-mix(in hsl,
					hsl(50 100% 50%),
					hsl(190 100% 40%) calc(var(--p) * 1%)
				);
			

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: */
			--not-glossy: calc(1 - var(--glossy));
			line-height: calc(
				var(--glossy)       * 1.1 +
				var(--not-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

Conclusion




Is it all a steaming pile of shit hacks?

API simplicity > Implementation simplicity

Yes, a lot of these were hacks. 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. Overcomplicated APIs cause a lot more collective pain than overcomplicated implementations, because there are always more API users than API developers. 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. 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. But ultimately, every case is different, and the final decision is yours.