6 posts on CSS Variables

Custom properties with defaults: 3+1 strategies

4 min read 0 comments Report broken page

When developing customizable components, one often wants to expose various parameters of the styling as custom properties, and form a sort of CSS API. This is still underutlized, but there are libraries, e.g. Shoelace, that already list custom properties alongside other parts of each component’s API (even CSS parts!).

Note: I’m using “component” here broadly, as any reusable chunk of HTML/CSS/JS, not necessarily a web component or framework component. What we are going to discuss applies to reusable chunks of HTML just as much as it does to “proper” web components.

Let’s suppose we are designing a certain button styling, that looks like this:

We want to support a --color custom property for creating color variations by setting multiple things internally:

.fancy-button {
	border: .1em solid var(--color);
	background: transparent;
	color: var(--color);
}

.fancy-button:hover {
	background: var(--color);
	color: white;
}

Note that with the code above, if no --color is set, the three declarations using it will be IACVT and thus we’ll get a nearly unstyled text-only button with no background on hover (transparent), no border on hover, and the default black text color (canvastext to be precise).

That’s no good! IT’s important that we set defaults. However, using the fallback parameter for this gets tedious, and WET:

Continue reading

Articles, Original, Tutorials, CSS, CSS Custom Properties, CSS Variables, Dynamic CSS
Edit post on GitHub

Dark mode in 5 minutes, with inverted lightness variables

6 min read 0 comments Report broken page

By now, you probably know that you can use custom properties for individual color components, to avoid repeating the same color coordinates multiple times throughout your theme. You may even know that you can use the same variable for multiple components, e.g. HSL hue and lightness:

:root {
	--primary-hs: 250 30%;
}

h1 {
	color: hsl(var(--primary-hs) 30%);
}

article {
	background: hsl(var(--primary-hs) 90%);
}

article h2 {
	background: hsl(var(--primary-hs) 40%);
	color: white;
}

Here is a very simple page designed with this technque:

Unlike preprocessor variables, you could even locally override the variable, to have blocks with a different accent color:

:root {
	--primary-hs: 250 30%;
	--secondary-hs: 190 40%;
}

article {
	background: hsl(var(--primary-hs) 90%);
}

article.alt {
	--primary-hs: var(--secondary-hs);
}

This is all fine and dandy, until dark mode comes into play. The idea of using custom properties to make it easier to adapt a theme to dark mode is not new. However, in every article I have seen, the strategy suggested is to create a bunch of custom properties, one for each color, and override them in a media query.

This is a fine approach, and you’ll likely want to do that for at least part of your colors eventually. However, even in the most disciplined of designs, not every color is a CSS variable. You often have colors declared inline, especially grays (e.g. the footer color in our example). This means that adding a dark mode is taxing enough that you may put it off for later, especially on side projects.

The trick I’m going to show you will make anyone who knows enough about color cringe (sorry Chris!) but it does help you create a dark mode that works in minutes. It won’t be great, and you should eventually tweak it to create a proper dark mode (also dark mode is not just about swapping colors) but it’s better than nothing and can serve as a base.

Continue reading


The -​-var: ; hack to toggle multiple values with one custom property

2 min read 0 comments Report broken page

What if I told you you could use a single property value to turn multiple different values on and off across multiple different properties and even across multiple CSS rules?

What if I told you you could turn this flat button into a glossy skeuomorphic button by just tweaking one custom property --is-raised, and that would set its border, background image, box and text shadows in one fell swoop?

How, you may ask?

The crux of this technique is this: There are two custom property values that work almost everywhere there is a var() call with a fallback.

The more obvious one that you probably already know is the initial value, which makes the property just apply its fallback. So, in the following code:

background: var(--foo, linear-gradient(white, transparent)) hsl(220 10% 50%);
border: 1px solid var(--foo, rgb(0 0 0 / .1));
color: rgb(0 0 0 var(--foo, / .8));

We can set --foo to initial to enable these “fallbacks” and append these values to the property value, adding a gradient, setting a border-color, and making the text color translucent in one go. But what to do when we want to turn these values off? Any non-initial value for --foo (that doesn’t create cycles) should work. But is there one that works in all three declarations?

It turns out there is another value that works everywhere, in every property a var() reference is present, and you’d likely never guess what it is (unless you have watched any of my CSS variable talks and have a good memory for passing mentions of things).

Intrigued?

It’s whitespace! Whitespace is significant in a custom property. When you write something like this:

--foo: ;

This is not an invalid declaration. This is a declaration where the value of --foo is literally one space character. However, whitespace is valid in every CSS property value, everywhere a var() is allowed, and does not affect its computed value in any way. So, we can just set our property to one space (or even a comment) and not affect any other value present in the declaration. E.g. this:

--foo: ;
background: var(--foo, linear-gradient(white, transparent)) hsl(220 10% 50%);

produces the same result as:

background: hsl(220 10% 50%);

We can take advantage of this to essentially turn var() into a single-clause if() function and conditionally append values based on a single custom property.

As a proof of concept, here is the two button demo refactored using this approach:

Limitations

I originally envisioned this as a building block for a technique horrible hack to enable “mixins” in the browser, since @apply is now defunct. However, the big limitation is that this only works for appending values to existing values — or setting a property to either a whole value or initial. There is no way to say “the background should be red if --foo is set and white otherwise”. Some such conditionals can be emulated with clever use of appending, but not most.

And of course there’s a certain readability issue: --foo: ; looks like a mistake and --foo: initial looks pretty weird, unless you’re aware of this technique. However, that can easily be solved with comments. Or even constants:

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

button { –is-raised: var(–OFF); /* … */ }

#foo { –is-raised: var(–ON); }

Also do note that eventually we will get a proper if() and won’t need such horrible hacks to emulate it, discussions are already underway [w3c/csswg-drafts#5009 w3c/csswg-drafts#4731].

So what do you think? Horrible hack, useful technique, or both? 😀

Prior art

Turns out this was independently discovered by two people before me:

And it was called “space toggle hack” in case you want to google it!


The Cicada Principle, revisited with CSS variables

4 min read 0 comments Report broken page

Many of today’s web crafters were not writing CSS at the time Alex Walker’s landmark article The Cicada Principle and Why it Matters to Web Designers was published in 2011. Last I heard of it was in 2016, when it was used in conjunction with blend modes to pseudo-randomize backgrounds even further.

So what is the Cicada Principle and how does it relate to web design in a nutshell? It boils down to: when using repeating elements (tiled backgrounds, different effects on multiple elements etc), using prime numbers for the size of the repeating unit maximizes the appearance of organic randomness. Note that this only works when the parameters you set are independent.

When I recently redesigned my blog, I ended up using a variation of the Cicada principle to pseudo-randomize the angles of code snippets. I didn’t think much of it until I saw this tweet:

This made me think: hey, maybe I should actually write a blog post about the technique. After all, the technique itself is useful for way more than angles on code snippets.

The main idea is simple: You write your main rule using CSS variables, and then use :nth-of-*() rules to set these variables to something different every N items. If you use enough variables, and choose your Ns for them to be prime numbers, you reach a good appearance of pseudo-randomness with relatively small Ns.

In the case of code samples, I only have two different top cuts (going up or going down) and two different bottom cuts (same), which produce 2*2 = 4 different shapes. Since I only had four shapes, I wanted to maximize the pseudo-randomness of their order. A first attempt looks like this:

pre {
	clip-path: polygon(var(--clip-top), var(--clip-bottom));
	--clip-top: 0 0, 100% 2em;
	--clip-bottom: 100% calc(100% - 1.5em), 0 100%;
}

pre:nth-of-type(odd) { –clip-top: 0 2em, 100% 0; }

pre:nth-of-type(3n + 1) { –clip-bottom: 100% 100%, 0 calc(100% - 1.5em); }

This way, the exact sequence of shapes repeats every 2 * 3 = 6 code snippets. Also, the alternative --clip-bottom doesn’t really get the same visibility as the others, being present only 33.333% of the time. However, if we just add one more selector:

pre {
	clip-path: polygon(var(--clip-top), var(--clip-bottom));
	--clip-top: 0 0, 100% 2em;
	--clip-bottom: 100% calc(100% - 1.5em), 0 100%;
}

pre:nth-of-type(odd) { –clip-top: 0 2em, 100% 0; }

pre:nth-of-type(3n + 1), pre:nth-of-type(5n + 1) { –clip-bottom: 100% 100%, 0 calc(100% - 1.5em); }

Now the exact same sequence of shapes repeats every 2 * 3 * 5 = 30 code snippets, probably way more than I will have in any article. And it’s more fair to the alternate --clip-bottom, which now gets 1/3 + 1/5 - 1/15 = 46.67%, which is almost as much as the alternate --clip-top gets!

You can explore this effect in this codepen:

https://codepen.io/leaverou/pen/8541bfd3a42551f8845d668f29596ef9?editors=1100

Or, to better explore how different CSS creates different pseudo-randomness, you can use this content-less version with three variations:

https://codepen.io/leaverou/pen/NWxaPVx

Of course, the illusion of randomness is much better with more shapes, e.g. if we introduce a third type of edge we get 3 * 3 = 9 possible shapes:

https://codepen.io/leaverou/pen/dyGmbJJ?editors=1100

I also used primes 7 and 11, so that the sequence repeats every 77 items. In general, the larger primes you use, the better the illusion of randomness, but you need to include more selectors, which can get tedious.

Other examples

So this got me thinking: What else would this technique be cool on? Especially if we include more values as well, we can pseudo-randomize the result itself better, and not just the order of only 4 different results.

So I did a few experiments.

Pseudo-randomized color swatches

https://codepen.io/leaverou/pen/NWxXQKX

Pseudo-randomized color swatches, with variables for hue, saturation, and lightness.

And an alternative version:

https://codepen.io/leaverou/pen/RwrLPer

Which one looks more random? Why do you think that is?

Pseudo-randomized border-radius

Admittedly, this one can be done with just longhands, but since I realized this after I had already made it, I figured eh, I may as well include it 🤷🏽‍♀️

https://codepen.io/leaverou/pen/ZEQXOrd

It is also really cool when combined with pseudo-random colors (just hue this time):

https://codepen.io/leaverou/pen/oNbGzeE

Pseudo-randomized snowfall

Lots of things here:

  • Using translate and transform together to animate them separately without resorting to CSS.registerPropery()
  • Pseudo-randomized horizontal offset, animation-delay, font-size
  • Technically we don’t need CSS variables to pseudo-randomize font-size, we can just set the property itself. However, variables enable us to pseudo-randomize it via a multiplier, in order to decouple the base font size from the pseudo-randomness, so we can edit them independently. And then we can use the same multiplier in animation-duration to make smaller snowflakes fall slower!

https://codepen.io/leaverou/pen/YzwrWvV?editors=1100

Conclusions

In general, the larger the primes you use, the better the illusion of randomness. With smaller primes, you will get more variation, but less appearance of randomness.

There are two main ways to use primes to create the illusion of randomness with :nth-child() selectors:

The first way is to set each trait on :nth-child(pn + b) where p is a prime that increases with each value and b is constant for each trait, like so:

:nth-child(3n + 1)  { property1: value11; }
:nth-child(5n + 1)  { property1: value12; }
:nth-child(7n + 1)  { property1: value13; }
:nth-child(11n + 1) { property1: value14; }
...
:nth-child(3n + 2)  { property2: value21; }
:nth-child(5n + 2)  { property2: value22; }
:nth-child(7n + 2)  { property2: value23; }
:nth-child(11n + 2) { property2: value24; }
...

The benefit of this approach is that you can have as few or as many values as you like. The drawback is that because primes are sparse, and become sparser as we go, you will have a lot of “holes” where your base value is applied.

The second way (which is more on par with the original Cicada principle) is to set each trait on :nth-child(pn + b) where p is constant per trait, and b increases with each value:

:nth-child(5n + 1) { property1: value11; }
:nth-child(5n + 2) { property1: value12; }
:nth-child(5n + 3) { property1: value13; }
:nth-child(5n + 4) { property1: value14; }
...
:nth-child(7n + 1) { property2: value21; }
:nth-child(7n + 2) { property2: value22; }
:nth-child(7n + 3) { property2: value23; }
:nth-child(7n + 4) { property2: value24; }
...

This creates a better overall impression of randomness (especially if you order the values in a pseudo-random way too) without “holes”, but is more tedious, as you need as many values as the prime you’re using.

What other cool examples can you think of?


Hybrid positioning with CSS variables and max()

4 min read 0 comments Report broken page

Notice how the navigation on the left behaves wrt scrolling: It’s like absolute at first that becomes fixed once the header scrolls out of the viewport.

One of my side projects these days is a color space agnostic color conversion & manipulation library, which I’m developing together with my husband, Chris Lilley (you can see a sneak peek of its docs above). He brings his color science expertise to the table, and I bring my JS & API design experience, so it’s a great match and I’m really excited about it! (if you’re serious about color and you’re building a tool or demo that would benefit from it contact me, we need as much early feedback on the API as we can get! )

For the documentation, I wanted to have the page navigation on the side (when there is enough space), right under the header when scrolled all the way to the top, but I wanted it to scroll with the page (as if it was absolutely positioned) until the header is out of view, and then stay at the top for the rest of the scrolling (as if it used fixed positioning).

It sounds very much like a case for position: sticky, doesn’t it? However, an element with position: sticky behaves like it’s relatively positioned when it’s in view and like it’s using position: fixed when its scrolled out of view but its container is still in view. What I wanted here was different. I basically wanted position: absolute while the header was in view and position: fixed after. Yes, there are ways I could have contorted position: sticky to do what I wanted, but was there another solution?

In the past, we’d just go straight to JS, slap position: absolute on our element, calculate the offset in a scroll event listener and set a top CSS property on our element. However, this is flimsy and violates separation of concerns, as we now need to modify Javascript to change styling. Pass!

What if instead we had access to the scroll offset in CSS? Would that be sufficient to solve our use case? Let’s find out!

As I pointed out in my Increment article about CSS Variables last month, and in my CSS Variables series of talks a few years ago, we can use JS to set & update CSS variables on the root that describe pure data (mouse position, input values, scroll offset etc), and then use them as-needed throughout our CSS, reaching near-perfect separation of concerns for many common cases. In this case, we write 3 lines of JS to set a --scrolltop variable:

let root = document.documentElement;
document.addEventListener("scroll", evt => {
	root.style.setProperty("--scrolltop", root.scrollTop);
});

Then, we can position our navigation absolutely, and subtract var(--scrolltop) to offset any scroll (11rem is our header height):

#toc {
	position: fixed;
	top: calc(11rem - var(--scrolltop) * 1px);
}

This works up to a certain point, but once scrolltop exceeds the height of the header, top becomes negative and our navigation starts drifting off screen:

Just subtracting --scrolltop essentially implements absolute positioning with position: fixed.

We’ve basically re-implemented absolute positioning with position: fixed, which is not very useful! What we really want is to cap the result of the calculation to 0 so that our navigation always remains visible. Wouldn’t it be great if there was a max-top attribute, just like max-width so that we could do this?

One thought might be to change the JS and use Math.max() to cap --scrolltop to a specific number that corresponds to our header height. However, while this would work for this particular case, it means that --scrolltop cannot be used generically anymore, because it’s tailored to our specific use case and does not correspond to the actual scroll offset. Also, this encodes more about styling in the JS than is ideal, since the clamping we need is presentation-related — if our style was different, we may not need it anymore. But how can we do this without resorting to JS?

Thankfully, we recently got implementations for probably the one feature I was pining for the most in CSS, for years: min(), max() and clamp() functions, which bring the power of min/max constraints to any CSS property! And even for width and height, they are strictly more powerful than min/max-* because you can have any number of minimums and maximums, whereas the min/max-* properties limit you to only one.

While brower compatibility is actually pretty good, we can’t just use it with no fallback, since this is one of the features where lack of support can be destructive. We will provide a fallback in our base style and use @supports to conditonally override it:

#toc {
	position: fixed;
	top: 11em;
}

@supports (top: max(1em, 1px)) { #toc { top: max(0em, 11rem - var(–scrolltop) * 1px); } }

Aaand that was it, this gives us the result we wanted!

And because --scrolltop is sufficiently generic, we can re-use it anywhere in our CSS where we need access to the scroll offset. I’ve actually used exactly the scame --scrolltop setting JS code in my blog, to keep the gradient centerpoint on my logo while maintaining a fixed background attachment, so that various elements can use the same background and having it appear continuous, i.e. not affected by their own background positioning area:

The website header and the post header are actually different element. The background appears continuous because it’s using background-attachment: fixed, and the scrolltop variable is used to emulate background-attachment: scroll while still using the viewport as the background positioning area for both backgrounds.

Appendix: Why didn’t we just use the cascade?

You might wonder, why do we even need @supports? Why not use the cascade, like we’ve always done to provide fallbacks for values without sufficiently universal support? I.e., why not just do this:

#toc {
	position: fixed;
	top: 11em;
	top: max(0em, 11rem - var(--scrolltop) * 1px);
}

The reason is that when you use CSS variables, this does not work as expected. The browser doesn’t know if your property value is valid until the variable is resolved, and by then it has already processed the cascade and has thrown away any potential fallbacks.

So, what would happen if we went this route and max() was not supported? Once the browser realizes that the second value is invalid due to using an unknown function, it will make the property invalid at computed value time, which essentially equates to the initial keyword, and for the top property, the initial value is 0. This would mean your navigation would overlap the header when scrolled close to the top, which is terrible!


Autoprefixing, with CSS variables!

1 min read 0 comments Report broken page

Recently, when I was making the minisite for markapp.io, I realized a neat trick one can do with CSS variables, precisely due to their dynamic nature. Let’s say you want to use a property that has multiple versions: an unprefixed one and one or more prefixed ones. In this example we are going to use clip-path, which currently needs both an unprefixed version and a -webkit- prefixed one, however the technique works for any property and any number of prefixes or different property names, as long as the value is the same across all variations of the property name.

The first part is to define a --clip-path property on every element with a value of initial. This prevents the property from being inherited every time it’s used, and since the * has zero specificity, any declaration that uses --clip-path can override it. Then you define all variations of the property name with var(--clip-path) as their value:

* {
	--clip-path: initial;
	-webkit-clip-path: var(--clip-path);
	clip-path: var(--clip-path);
}

Then, every time we need clip-path, we use --clip-path instead and it just works:

header {
	--clip-path: polygon(0% 0%, 100% 0%, 100% calc(100% - 2.5em), 0% 100%);
}

Even !important should work, because it affects the cascading of CSS variables. Furthermore, if for some reason you want to explicitly set -webkit-clip-path, you can do that too, again because * has zero specificity. The main downside to this is that it limits browser support to the intersection of the support for the feature you are using and support for CSS Variables. However, all browsers except Edge support CSS variables, and Edge is working on it. I can’t see any other downsides to it (except having to use a different property name obvs), but if you do, let me know in the comments!

I think there’s still a lot to be discovered about cool uses of CSS variables. I wonder if there exists a variation of this technique to produce custom longhands, e.g. breaking box-shadow into --box-shadow-x, --box-shadow-y etc, but I can’t think of anything yet. Can you? ;)