Dynamic CSS

…with custom properties

Slides for Frontend Masters Course “Dynamic CSS”

By Lea Verou (@LeaVerou)

Lea Verou @leaverou

Prerequisites

What are we going to learn?

What will you need?

A whole day discussing …CSS variables? Really?!

Variables in


		accent-color: #f06;
		heading-font: Fancy Webfont, serif;

		h2 {
			color: accent-color;
			font-family: heading-font;
		}
	
You may come from a Sass or Less background, where variables are used like this…

So many refactor to this…


		:root {
			--accent-color: #f06;
			--heading-font: Fancy Webfont, serif;
		}

		h2 {
			color: var(--accent-color);
			font-family: var(--heading-font);
		}
	
but CSS variables can do so much more!
So people often migrate to CSS variables by just changing syntax. And it works, for the most part. But it's like hiring a Michelin star chef to fry eggs. CSS Custom properties can do so much more!
CSS variables are scoped on elements, not curly bracket blocks, taking full advantage of the browser’s DOM structure knowledge. This means that we can set them and use them in entirely unrelated parts of our CSS, and things just work beautifully. Take a look at this example. How would you do it in Sass? The same code using Sass variables would produce a [syntax error](https://codepen.io/leaverou/pen/3ae47e6e2b1587c66d6b2545ab26de79?editors=1100). Try setting the corner size via an inline style. That is definitely something you can't do with preprocessors! You could also use any unit you want, even viewport units! ------- *(We revisit this slide after discussing fallbacks)* Note that these cutout corners can only be 45 degrees, we cannot have different horizontal and vertical offsets. How can we change this example so that we can also use `--corner-size-x` and `--corner-size-y` (either both or just one)? Does this work like a shorthand? If not, what's the difference?

Sass variables are scoped on {…} blocks, CSS variables are scoped on elements
(lexical vs dynamic scope)

I thought we weren’t supposed to call them CSS variables?

* Short answer: *it doesn't matter, use whatever term you want* * [The spec itself](https://drafts.csswg.org/css-variables/) uses both. * Yes, they are custom properties, but they can be used as reactive variables too. Certain use cases naturally make more sense with one or the other term, others work with both. * I will be using both terms throughout this workshop. * Some people have suggested using the term "custom property" when setting (`--foo: ...`) and "CSS variable" when getting (`var(--foo)`). Feel free to adopt this if it resonates with you. Or don't. *It doesn't actually matter.*

What’s up with :root?

Getting to the root of :root


			/* 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;
			}
		
In the past, one reason to prefer `:root` was also that there were scripts like [postcss-custom-properties](https://github.com/postcss/postcss-custom-properties) that only preprocessed custom properties in `:root`. However, these days this is no longer relevant, not only because browser support for custom properties is great so the need to preprocess is pretty much gone, but also because there are [PostCSS plugins](https://www.npmjs.com/package/postcss-css-variables) that do heavier transformations and are not just restricted to `:root`.

What CSS variables can’t do

Valid Sass code Invalid CSS code
Property names

						$prop: margin-top;
						#{$prop}: 10px;
					

						--prop: margin-top;
						var(--prop): 10px;
					
Selectors

						$i: 1;
						.foo-#{$i} {
							...
						}
					

						:root { --i: 1; }
						.foo-var(--i) {
							...
						}
					
@rules

						$w: 500px;
						@media (min-width: #{$w}) {
							...
						}
					

						:root { --w: 500px; }
						@media (min-width: var(--w)) {
							...
						}
					
This entire workshop will go in depth about how CSS variables are more powerful than preprocessor variables. So it's only fair that before we start we briefly discuss what they *can't* do.

Inheritance

The browser is aware of our DOM structure, and custom properties inherit by default. Here we have defined our properties on the `:root` selector, but they are available on every other element. But even better, we can *override* variables on individual elements and it's the more specific value that inherits down. Let's make any blog post with the `.alt` class use the secondary color instead. ------ One of the major ways we take advantage of this inheritance is media queries. We can only overide a few variables in the media query and have all our styles adjust instead of rewriting a bunch of rules. Let's see how this works in practice with a dark mode version of this website. [Codepen solution](https://codepen.io/leaverou/pen/deee6211f81af64b1a5ebb70f53e044e?editors=1100) ------ Remember the clumsy dark mode we used here? Let's improve it!

CSS Variables are actually
CSS properties

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;
					}
				}
			

What if we don’t want inheritance?

Sometimes inheritance is not desirable. One common pattern for using custom properties more as properties is to use them on very liberal selectors, e.g. `input` or even `*` and then specify them only when we want to trigger the corresponding effect. Often, these effects should not be inherited. One way to disable inheritance is to just make sure the property always has a non-inherited value. `initial` is perfect for that: it resets the property to its initial value, as if it had not been set at all, but still prevents it from inheriting. We can always opt-in inheritance on a case-by-case basis by using the `inherit` keyword. -------- There is a newer, more explicit way to disable inheritance: the [@property](https://developer.mozilla.org/en-US/docs/Web/CSS/@property) rule. It makes custom properties tremendously more powerful, and we will keep revisiting it throughout this workshop.

You can disable inheritance by setting the property to `initial` on `*`

`@property` allows us to register our properties and control how they behave

Remember this?


			CSS.registerProperty({
				name: "--corner-size",
				syntax: "*";
				inherits: false
			})
		
Registering custom properties, started as a JS API, and we got the declarative `@property` rule later.
Chrome Firefox Edge Safari
@property 85 85
CSS.registerProperty() 79 79
However, today there is only a very marginal benefit to using the JS API, as it's supported by the same browsers, it just shipped in a slightly older version compared to `@property`. Given that most users of these browsers have updated to a version that supports `@property` at this point, you would only be getting a small increase in support, for a significant increase in maintainability cost. Also, Safari has [hinted](https://github.com/w3c/css-houdini-drafts/issues/940) that they may implement `@property` *first* due to its improved performance. The JS API is still useful for debugging, because it actually prints out readable errors, while the CSS rule fails silently.
Since custom properties are inherited properties, they also inherit down Shadow DOM boundaries, and they are not affected by any potential `all: unset` CSS resets. Users of Web components cannot directly style them (that’s the whole point of encapsulation), but custom properties can provide style hooks for customizing aspects of the component. Photo by Jari Hytönen on Unsplash
Even as consumers of components, we can use custom properties to create our own higher-level abstractions for styling components. Let's take a look at this example. We want to change the divider color, but the component does not expose a custom property for this. Instead, it exposes two [parts](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) of the component: `divider` and `handle`. We can define a custom property that allows us to set the color of both at once without having to drill down to the parts. Note that this technique is not just limited to actual web components, it can be useful for any reusable blob of HTML & CSS (which is essentially what a component is).
The same technique allows us to define custom properties that style native components, so we can avoid using messy prefixed pseudo-elements all over the place. Here we have a rather simple example of styling a `<progress>` element. Every time we want to change the color of the progress bar, we have to override two separate rules. Custom properties can help abstract away this ugliness.

Custom properties inherit down Shadow DOM boundaries
(and are not reset by all: initial)

Unlike other inheritable properties however, custom properties cannot be reset by `all: initial`.

Fallbacks

How do these fallbacks actually work?


--accent-color: yellowgreen

No --accent-color set

--accent-color: initial

No CSS variables support

			background: red;
			background: var(--accent-color, orange);
		

The `var()` fallback is applied when there is no value, or when it resolves to `initial`
(and some other cases we'll see later)

The `var()` fallback is no help in browsers that don’t support CSS variables

Chrome Firefox Edge Safari
CSS Variables 49 31 15 9.1
For most cases, providing fallbacks via the cascade works fine to provide a baseline accessible experience to the few browsers that still don’t support custom properties. However, if you need a more elaborate fallback, you can use the [`@supports`](https://developer.mozilla.org/en-US/docs/Web/CSS/@supports) rule. Any custom property and any value will do as the test. When do you need a more elaborate fallback? Typically when you need to control more properties and/or rules than the ones with the `var()` reference. Avoid using its negative form (`@supports not (--css: variables)`), as that will prevent you from targeting the really old browsers (notably, IE 11 or under) that don’t even support `@supports`. ---------- Can we use `@supports` to detect `@property` support? Let's try.

Use `@supports` to provide more elaborate fallback styling
And avoid @supports not (…)

Chrome Firefox Edge Safari
CSS Variables 49 31 15 9.1
@supports 28 22 13 9

There is currently no way to use `@supports` to detect `@property` support

Why does this not work? We'll see in the next section, when we talk about parse time vs computed time values.

How can we detect @property then?


			if (window.CSSPropertyRule) {
				let root = document.documentElement;
				root.classList.add("supports-atproperty");
			}
		

Multiple fallbacks?

background: var(--nonexistent, none, yellowgreen);

				/* Resolves to: */
				background: none, yellowgreen;
			
Anything after the comma is part of the fallback, including …other commas.

Fallbacks can be dynamic, via `var()` references

Default values allow us to create “shorthands” of sorts
(but careful, they’re not real shorthands!)

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.

DRY fallback strategies

  1. Fallback in a variable
    
    					--color-initial: black;
    					...
    					color: var(--color, var(--color-initial));
    				
  2. Pseudo-private properties
    
    					--__color: var(--color, black);
    					...
    					color: var(--__color);
    				
The first way allows you to use the same property internally that you expose to other people, but it makes each usage far more verbose. The second way is more concise, but it suffers from the wart that you have to use a different property internally than the one you expose and it's only private by convention.

DRY fallback with `@property`


		@property --color {
			syntax: "<color>";
			initial-value: black;
			inherits: true;
		}
		...
		color: var(--color);
		
In most cases, `@property` offers a far better solution, with the main drawback being its browser support. However, do note that the other approaches allow for different defaults per use, whereas an `@property` rule is global.

CSS Variables enable theming
independent of CSS structure

This is one of the most important takeaways from today. When custom properties are used as styling hooks, a component can change its internal CSS structure entirely, without anyone needing to change their code that uses and customizes said component.

Invalid Values


--accent-color: 42deg;

			background: red;
			background: var(--accent-color, orange);
		

Otherwise valid CSS values may make declarations
Invalid At Computed Value Time (IACVT)

What is a Computed Value?

* The computed value is the value that is transferred from parent to child during inheritance. For historical reasons, it is not necessarily the value returned by the `getComputedStyle()` function, which sometimes returns used values. * In general, the computed value resolves the specified value as far as possible without laying out the document or performing other expensive or hard-to-parallelize operations, such as resolving network requests or retrieving values other than from the element and its parent. * Read more: - [Computed Values](https://www.w3.org/TR/css-cascade-4/#computed) - [Used Values](https://www.w3.org/TR/css-cascade-4/#used)

IACVT → `unset`

So what does IACVT mean in practice? A property that is IACVT is treated as being set to [`unset`](https://developer.mozilla.org/en-US/docs/Web/CSS/unset). If you haven't heard of it, `unset` is a very useful keyword that resolves to `initial` for non-inherited properties, and `inherit` for inherited properties. E.g. `* { all: unset; }` would be the most quick & dirty CSS reset 😉

--accent-color: initial;

			background: red;
			background: var(--accent-color, 42deg);
		

Fallbacks can also trigger IACVT

Are these equivalent?


			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:

We are used to the classic cascade behavior, where if a value is not supported, the browser just discards the entire declaration and falls back to any preceding declarations for the same property. This happens at parse time. This does not work with custom properties. Since their values are resolved at computed value time, the browser has already thrown away any preceding declarations. This is exactly why the concept of Invalid At Computed Value Time (IACVT) was introduced.
It would be reasonable to expect that if we register our custom property it would throw away invalid values during parse time, so we can still use the cascade for fallbacks. Unfortunately, that's not the case. Registered properties only enforce their syntax at computed value time, and become IACVT if they do not match it. The reason for this is that otherwise, registering new properties (which could happen at any time), would trigger a reparse of the entire stylesheet.

The syntax of registered properties is checked at computed value time, not parse time

This is why we cannot detect `@property` through `@supports`: `@supports` checks for parse-time syntax.

Then how do we provide fallbacks for unsupported features??
@supports to the rescue!

What about this?


			font-size: 20px;
			font-size: clamp(16px, 100 * var(--font-size-scale), 24px);
		

What happens in browsers that don’t support clamp()?

Because when parsing, the browser doesn't have the values of custom properties, it cannot know if after substitution, the resulting values will be valid. Therefore, it optimistically assumes they will be valid, and throws away any other fallback values. So, even though the unsupported function is right there, and not inside a variable, like in our previous examples, once `var()` is introduced, these types of fallbacks go out of the window, and `@supports` is the only way.

Customizable bar chart component

A
B
C
D
E
F
1. **Base**: Fill in the CSS missing to utilize the `--p` variables for the percentages. You will find Flexbox helpful for the layout. 2. **Going further:** Introduce properties to further customize the chart. Some ideas: * `--bar-color` * `--bar-width` * `--bar-spacing` * `--band-count` 3. Question: Why use `--bar-color` or `--bar-width` when we could just override the corresponding properties?
Remember this? Now each article utilizes both its primary and secondary colors, which means our tweak for `article.alt` doesn’t work well anymore. How can we swap `--primary-color` and `--secondary-color`?

Cycles make all variables in the cycle IACVT

What CSS variables can’t do

Valid Sass code Invalid CSS code

							:root {
								$depth: 0;


								& > * {
									$depth: $depth + 1;
								}
							}
						

							:root {
								--depth: 0;
							}
							:root > * {
								/* ⚠️ Cycle! */
								--depth: calc(var(--depth) + 1);
							}
						
In Sass (and in JS!) variables are *imperative*. There is an order of steps in their calculation, and expressions involving multiple variables are executed once. CSS variables are *reactive*: there is no order of steps, and expressions involving multiple variables are recalculated every time a value changes. In imperative variables, an assignment can include the variable itself, as it can operate in its previous value. In reactive variables, if an assignment refers to the variable being set, that is a *cycle*.

Combining Sass and CSS variables

Sass Compiled CSS code

							:root {
								$depth: 0;
								--depth: #{$depth};

								& > * {
									$depth: $depth + 1;
									--depth: #{$depth};
								}
							}
						

							:root {
								--depth: 0;
							}

							:root > * {
								--depth: 1;
							}
						
Until `parent()` arrives (which will be a while!), you can combine Sass (or Less) and CSS variables to get the best of both worlds. Later on we will see a case where this is useful. Note that this only goes one way: you can set CSS variables to Sass variables, but you cannot set Sass variables to CSS variables, as the preprocessor does not have enough context to compute them.

Flexible pie chart

Flexible pie chart

Open in new tab

Here we have a hardcoded pie chart, which is not that useful. We want to make it into a reusable pie chart component, allowing the values and colors to be customized via `--colorN` and `--valueN` variables, with appropriate defaults so they don't all need to be set at once (if a color is not set, it should be `hsl(N * 25 80% 50%))` where N is the index of the slice). How can we do that? [Codepen solution](https://codepen.io/leaverou/pen/036a5e69bf8c0929b182df5a6dbac6b8) The pure CSS solution works, but is very repetitive. How can we use Sass (or any other preprocessor) to eliminate the repetition? Useful links: - [Sass variables](https://sass-lang.com/documentation/variables) - [Sass interpolation](https://sass-lang.com/documentation/interpolation) - [Sass @for loops](https://sass-lang.com/documentation/at-rules/control/for) - [Codepen solution](https://codepen.io/leaverou/pen/c701c26e89a4e964e14e4b2ca0a6845a)

Space Toggle


--accent-color:;

--accent-color: ;

			background: red;
			background: var(--accent-color, orange);
		

--accent-color:;

--accent-color: ;

			background: red;
			background: linear-gradient(white, transparent)
			            var(--accent-color, orange);
		
While in our previous example it looked like the empty value and a value consisting entirely of whitespace would produce the same result, that is not always the case. Whitespace is a perfectly valid value for a custom property, but depending on how it's used, it may make the resulting declaration invalid (as in the previous case, `background: ;` is invalid), triggering IACVT. In fact, whitespace is very useful for optionally enabling parts of a value as we will see later.

Whitespace is perfectly valid, and useful in turning parts of a value off

Here we have used a bunch of `--box-shadow-*` properties to emulate longhands for `box-shadow`. They all have fallbacks except `--box-shadow-blur`, so that that property can be used to "activate" the shadow (otherwise every single element would have a shadow). Notice that this is missing a `--box-shadow-inset`. How can we add that? What fallback do we use?

Whitespace is useful as a fallback too

Whitespace is far more useful than meets the eye. Here we have a button with a "shiny" variation that has a gradient. However, this means that to make a button shiny, we need to change the HTML, which isn't always feasible. What if we could use a `--is-shiny` property to control whether our button is shiny? We can take advantage of the fact that whitespace is a valid value for every property, and use whitespace to turn off parts of a value, and the `var()` fallback to specify what these parts are. Then `initial` turns all these fallbacks on in one fell swoop. We can even alias ` ` and `initial` to `--ON` and `--OFF` to make the code more readable. This is called "space toggle". It's a hack, but sometimes it's the only way to do things. Like this example? You can find a more elaborate one [here](https://lea.verou.me/2020/10/the-var-space-hack-to-toggle-multiple-values-with-one-custom-property/).

Turn off multiple different parts of custom properties in one go with whitespace
(Space Toggle)

Responsive Design

Custom properties make responsive design easier, but there are many different ways to use them. The obvious way is using variables in our base CSS, and overriding them as needed in the media query. This tremendously reduces the amount of overrides needed, making code far more maintainable. ------------ However, it does mean we still need to hunt down in multiple places for the value of each style. Another strategy to keep everything together is to use different properties for each breakpoint, e.g. `--font-size-large` When the adjustment is relative, it means that we need to adjust both when the base value changes. One way to avoid this is to define optional *scaling factors* in our MQs, and use them defensively in our base CSS. [Codepen solution](https://codepen.io/leaverou/pen/77d09a1015c57d8d6f40c0fbfda3d839?editors=1100)

RWD strategy 1

Base CSS uses var() and sets defaults, MQs override


			margin: var(--gutter);
			padding: calc(.6em + var(--gutter) * 2);
		

RWD strategy 2

Base CSS sets different custom properties for each breakpoint, MQs use them


			font-size: 90%;
			--font-size-large: 110%;
		

RWD strategy 3

Base CSS multiplies by optional scaling factor, MQs set said factor


			font-size: calc(90% * var(--font-size-scale, 1));
		
Want to dig in more? [This article](https://css-tricks.com/responsive-designs-and-css-custom-properties-defining-variables-and-breakpoints/) by Mikolaj Dobrucki is quite good. Note that these techniques are useful every time you need to create variations of a design, not just for RWD. E.g. multipliers can be useful when you have multiple instances of a component with different content, and need to vary some of them. In fact, these slides use a similar technique to vary the font size of the live demos!

Colors

Abstracting away colors can be useful to make it easier to change an entire theme by only changing a few variables. However, it needs to be done with care, because colors also have meaning. Here, we use red to signify that an action is destructive, a common convention. What happens if in a future redesign, the secondary accent color becomes green? Now the UI communicates the *opposite* message! The problem is that there is a mismatch between our *intent* and our code. Our code says "give me any accent color", while our intent was to get a red color that fits in nicely with the current theme, regardless of whether it's the primary or secondary color. One strategy to avoid this is to use *aliases*: in the same place you set your accent colors, you can also set hue-based variables. Then you use the accent color variables where you just need an accent color, and the hue based ones when you need a specific hue to communicate something.

Use hue-based aliases of accent colors to make intent clear

--color-red: var(--color-primary);
Here we are using a variety of hacks to create color variations: `filter: brightness()` or semi-transparent white overlays. [Soon](https://drafts.csswg.org/css-color-5) we will be able to manipulate colors directly, but currently this is only supported in Safari (and very partially Firefox). What can we do meanwhile? Define more granular custom properties, what else? 😁 Let's first go the more verbose but clear way: defining individual properties for each component. Be careful of inheritance caveats! How can we ensure that the resulting color is still readable? -------------- Can we eliminate verbosity? We can store the hue and saturation in the same property. We can also store lighter and darker variations and use those throughout. --------------- What happens if we want to make the link color lighter on hover? With this approach, we'd need to redefine the entire color. Let's look at a slightly different approach. Instead of specifying the link lightness inline, we could assign it to a variable as well, a local one. Then on hover, we can just override the variable.

We can specify each color component via separate variables and do math with it
(but it's verbose AF)

Use `clamp()` to ensure readability

We can store multiple color components in the same variable

`--background-color-*` and `--text-color-*` for independent tweaking

Here we have some repeated HTML to display some data, which could have been generated from a server-side templating script. We are using an `--intensity` CSS variable to communicate the migraine intensity to the CSS so it can be used for styling. Let's try to modify the CSS so that stronger migraines are more red and lighter ones are more yellow! We want `--intensity: 0` to correspond to a hue of `50` (warm yellow) and `--intensity: 5` to correspond to a hue of `0` (pure red).

We can use variables to set color components to create dynamic palettes

To provide a fallback for an entire calculation, use an intermediate variable

Color manipulation, today

- Separate `--primary-color-*` properties - Hue and saturation can be stored together - Precomputed variations: `--primary-color-lighter`, `--primary-color-darker` etc - `--background-color-*` and `--text-color-*` Properties

All of the above can coexist!

The future is colorful

- Custom properties that store only colors - Functions to manipulate colors - `color-mix(in lch, var(--color) 80%, white)` - Relative syntax: `hsl(from var(--color) h s 80%)` - Better color spaces for adjustment: `lch(from var(--color) 80% c h)`

Transitions & Animations

[CSS variables] can even be tran­sitioned or ani­mated, but since the UA has no way to interpret their con­tents, they always use the "flips at 50%" behav­ior that is used for any oth­er pair of val­ues that can’t be intelligently interpolated.

CSS Custom Properties for Cascading Variables Module Level 1

Custom properties can trigger transitions but do not interpolate by default

*Registered* custom properties can interpolate!

Here we are animating from one color to another. Let's use our two main colors instead of repeating them. This works just fine. Animating the custom properties themselves requires registration however.

`var()` in keyframes works just fine

Interpolating parts of non-interpolatable values is a prime use case for animating custom properties, and gradients are possibly the most common such case. Even if gradients *were* interpolatable, animating custom properties is usually preferable, as it allows for more [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) code. Let's modify this code to do a cool image reveal! Interested in more examples of animating gradients? Ana Tudor has written [a whole article](https://css-tricks.com/the-state-of-changing-gradients-with-css-transitions-and-animations/) about it. And when it comes to animating custom properties in general, [this article](https://css-tricks.com/exploring-property-and-its-animating-powers/) by Jhey Tompkins is full of cool examples and intersting techniques.
Custom properties can help with vendor prefixes. For example, `backdrop-filter` still needs a `-webkit-` prefix in [certain browsers](https://caniuse.com/css-backdrop-filter).

CSS Variables enable you to
set multiple properties at once
but they do not interpolate!

Numbers & Lengths

Here we have an element whose aspect ratio we want to mirror the aspect ratio of our viewport. To avoid repeating the fraction of the viewport its dimensions are twice, it's a reasonable thought to try and abstract that into a variable. However, simple concatenation like in Sass does not work to get a `<length>` out of that. ---------- We can still convert, but we need to use `calc()`. Yes, `calc()` is a little verbose. Another idea idea may be to specify the entire width in the variable, then only use `calc()` in `height`. It's a good idea, but does not work yet. -------- Now let's try to display the number we have through generated content. Why doesn't it work? How can we fix that?

Variable values cannot contain parts of tokens

If you write `var(--p)vw`, the browser understands `50 vw`, not `50vw`

Variable values are token lists

What’s a token?

none no-repeat 42.1 12px 360deg 5% , / rgb( image-set( ) #ff0066 #container "YOLO" url(kitten.jpg)          
Want to know more? Here is [the source](https://drafts.csswg.org/css-syntax-3/#token-diagrams)!

Valid or invalid?


			--type: linear-gradient(;
			background: var(--type) white, black );
		
Even though `linear-gradient(` is a single token, this will not work because variable values [cannot contain mismatched parens or braces](https://drafts.csswg.org/css-variables/#syntax)

Variable values cannot contain unmatched `()`, `[]`, or `{}`
and they severely break parsing!

Valid or invalid?


			--stops: white, black,;
			background: linear-gradient( var(--stops) red);
		
This works fine, because `,` is just another token

Valid or invalid?


			--to: to;
			background: linear-gradient( var(--to) right, white, black );
		
This works fine, because `to` is just another token
A useful application of the fact that variable values can contain multiple tokens, is in creating "single property mixins", i.e. prefilling part of the value of a property.

CSS Variables let you create
single property mixins
(like function currying)

Number → unit: calc(var(--foo) * 1px)
Unit → number:

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.
Here we have a dark variation with many overrides, which requires a class change. Can we use a single `--is-dark` variable for this?

You can use 0/1 toggles for any numeric value


			--if-not-foo: calc(1 - var(--if-foo));
			property: calc(
				var(--if-foo)     * value_if_true +
				var(--if-not-foo) * value_if_false
			);
		
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
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?

0 - 1 Range mapping


			property: calc(
				         var(--p)  * min +
				calc(1 - var(--p)) * max
			);
		
The same formula as 0/1 toggles actually works for entire 0 - 1 ranges, as it's actually just the linear interpolation formula. Note that min and max here just represent the value you want to get if `--p` is 0 and 1, respectively. It is not necessary that min ≤ max.
Using abstract 0 to 1 or -1 to 1 ranges can also be immensely useful when we want the same variable to control multiple different things. For example here, we want the gradient color stop to go from `0%` to `100%` while the horizontal text shadow offset goes from `1em` to `-1em`. Note that our min and max variables do not necessarily satisfy min < max!

General linear range mapping

If our variable is x, we basically want to find a and b such that
a × min_x + b = min_y (1)
a × max_x + b = max_y (2)
We subtract (2) from (1) to find a:
  1. a × (max_x - min_x) = max_y - min_y
  2. a = (max_y - min_y) / (max_x - min_x)
Then we substitute a into (1) to find b:
  1. b = min_y - a × min_x
  2. b = min_y - (max_y - min_y) / (max_x - min_x) × min_x

Range mapping allows the same variable to control multiple things

Strings

Here we want to prepend and append an emoji after certain headings, so we tried using a custom property to specify what the emoji should be. However, it doesn’t seem to be working. What did we do wrong?

If your custom property contains a string, don’t forget the quotes!

`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.
Let's try to use this trick to display a percentage on each bar. What problem do you see? ---------- We can convert any number to an integer by assigning it to a property registered as `<integer>` and doing some (even trivial) math with it to trigger the conversion.

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.

Images


					--img: "cat1.jpg";
					background: url("img/" var(--img));
				
CSS limitation

					--img: "img/cat1.jpg";
					background: url(var(--img));
				
CSS bug

					--img: url("img/cat1.jpg");
					background: var(--img);
				
Works!
The first case doesn't work because CSS lacks string concatenation syntax. You may be familiar with concatenating strings in the `content` property by just placing them next to each other, but that is syntax specific to that property, not a CSS wide concatenation mechanism. Note that there is [consensus to add such syntax in some form](https://github.com/w3c/csswg-drafts/issues/542), though the exact details have not been worked out yet. The second case doesn’t work due to a …bug in CSS itself. When `url()` was introduced in CSS 1, it allowed both quoted and unquoted URLs. However, this flexibility comes at a hefty price: to parse `url()` tokens, if their parameter is not a quoted string, everything within needs to be assumed to be a relative URL, and that includes `var()` references! In this case, the unquoted `(` inside `url()` will cause a parse error, so the entire declaration is thrown out as invalid. There is a new function that was recently introduced, [`src()`](https://drafts.csswg.org/css-values-4/#urls), which will fix this by only allowing strings to specify the URL, however there are no implementations yet. Including the entire `url()` token in the variable works, though as we will see in the next few slides, it's not without its warts.

Custom properties can only contain entire `url()` tokens, not parts thereof
(for now)

How do relative URLs resolve?


			/* a.css */
			--img: url("a.png");

			/* b.css */
			background-image: var(--img);
		

Let’s check out what happens!

If you’re interested in the reasoning behind the decision, [csswg-drafts#757](https://github.com/w3c/csswg-drafts/issues/757) is the thread to dig into!

Relative URLs in custom properties resolve at the point of usage

Registering as "<url>" or "<image>" will allow us to change this in the future (but doesn’t work yet)

JS


				// 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);
			
Here we have a radial gradient background whose center is hardcoded to the center of the page. However, we want it to follow the mouse, for a subtle (with suitable colors) spotlight effect. With a little bit of JS, we can set `--mouse-x` and `--mouse-y` properties to communicate the mouse position to CSS. This already gives us a big advantage over pre-custom property CSS, where we'd need to set the entire gradient in JS, and tweak JS code every time we wanted to make a design change.
If we set `--mouse-x` and `--mouse-y` based on pixels, what happens here? How can we set `--look` based on that? Setting things based on pixels makes them less reusable. What if we instead set `--mouse-x` and `--mouse-y` based on percentage of screen? -------- Then we can still get a length by multiplying with `100vw` and `100vh` respectively, but we can also just use the percentage directly.

Reusable pointer coordinates


			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);
			});
		

Prefer abstract 0 to 1 percentages than absolute pixel lengths

0-1 can be converted to a length:

calc(var(--mouse-x) * 100vw)

…but the reverse isn’t possible

What about local coordinates?

Getting local pointer coordinates


			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);
		
Here we are generically setting local pointer coordinates on an element. Let's change the background gradient to draw a circle. Note that we did not need to change anything in the JS. Can we use this code as a starting point to draw a "ripple effect", similar to [that of Material Design](https://material.io/components/buttons)? If you're not familiar with it, this means that after clicking, the semi-transparent white circle grows in radius until it fills the entire button and simultaneously decreases in opacity until it fades out. ([Codepen solution](https://codepen.io/leaverou/pen/acb3c50c99c89ed9aec9f141c6b1653a)).
# A note on performance - Event delegation (monitoring events on `document`) gives you maximum flexibility and maintainability, but can be slower - Scoping to specific (few) elements can be more performant, but is a tighter coupling of CSS and JS - For anything other than `*move`, err on flexibility - For `*move`, it depends. I'd err on flexibility, measure, adjust if needed.
See also [this article](https://nolanlawson.com/2019/08/11/high-performance-input-handling-on-the-web/).

Range mapping is also useful to avoid registering a ton of properties


			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);
			});
		
Let’s try to recreate a common "typewriter" effect for this heading. Our width is set in `ch` units. `1ch` is the width of the `0` glyph. That is not super useful in general, but in monospace fonts, that gives us the width of *every* character. So, we have managed to recreate the effect, but we had to hardcode the length of the heading. CSS variables can help us make this more widely useful. ----------- What happens if the text can change after initial load? Either our code would need to take care of updating the `--length` variable, or (to avoid such tight couplings) we can use [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).

Passing element content length


			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});
		
# A note on performance - This has been fun, but event delegation on `scroll` is *usually* a Bad Idea™ - Trade in some flexibility for performance and scope it to specific elements
See also [this article](https://nolanlawson.com/2019/08/11/high-performance-input-handling-on-the-web/).

CSS Variables are a revolution for
separation of style and behavior