Dark mode in 5 minutes, with inverted lightness variables

Lea Verou shows a method to implement dark mode, not by swapping entire colors, but by simply changing their lightness

The basic idea is to use custom properties for the lightness of colors instead of the entire color. Then, in dark mode, you override these variables with 100% - lightness. This generally produces light colors for dark colors, medium colors for medium colors, and dark colors for light colors, and still allows you to define colors inline, instead of forcing you to use a variable for every single color.

For best results she also taps into LCH colors.

LCH is a much better color space for this technique, because its lightness actually means something, not just across different lightnesses of the same color, but across different hues and chromas.

Dark mode in 5 minutes, with inverted lightness variables →

Style Pseudo-elements with Javascript Using Custom Properties

Over at CSS { In Real Life }, author Michelle Barker has detailed a clever way to style pseudo-elements (such as ::before and ::after) through JavaScript.

In Javascript we have a few ways of selecting elements, but we can’t directly target pseudo-elements. […] Luckily, CSS custom properties can help.

👉 If you set a custom property on the element that “owns” the pseudo-element the pseudo-element itself can pick it up, thus enabling a way to style it.

Quick Tip: Style Pseudo-elements with Javascript Using Custom Properties →

The Power of Composition with CSS Custom Properties

Maxime Heckel, while revamping the theme on his blog:

I stumbled upon this pattern of composing CSS partial values for my HSLA colors, assign them to variables. Thanks to this, I was able to build my fairly complex theme and a full color palette that made sense for my websites by only using CSS. No ThemeProvider. No weird CSS-in-JS tools or hacks. Just plain old CSS. Moreover, it integrated perfectly with my existing React/CSS-in-JS setup and helped me declutter quite a few lines of style spaghetti code.

Ever since I learnt that you can store anything in CSS Custom Properties this is something that I too have been doing more and more. And thanks to the use of space-separated functional color notations this has become even more easy to do.

I also like the term “Partial Values” he uses there. Describes ‘m perfectly!

The Power of Composition with CSS Variables →

Make the page count of a 3D book visible using CSS Custom Properties

Michael Scharnagl:

I am currently building a book section for this site and thought it would be cool to show the books in 3D and also to make it visible how many pages a book has. In this article I would like to show you how to use CSS custom properties to adapt the thickness of a 3D book showing how many pages the book has.

Using a --page-count custom property he defines the number of pages, which is then clamped between a minimum and a maximum (--page-count-range), and finally converted to a width in pixels (--page-width). The resulting <length> is then applied onto a 3D-rotated div that makes up the thickness of the book.

html {
  --page-count: 50;
  --page-count-range: clamp(1, calc(var(--page-count) / 50), 20);
  --page-width: calc(10px * var(--page-count-range));
}

.book__wrapper::before {
  width: var(--page-width);
  transform: translateX(calc(200px - var(--page-width) / 2 - 3px)) rotateY(90deg) translateX(calc(calc(var(--page-width)) / 2))
}

.book__wrapper::after {
  transform: translateZ(calc(var(--page-width) * -1));
}

Make the page count of a 3D book visible using CSS Custom Properties →
Demo →

How to Play and Pause CSS Animations with CSS Custom Properties

Mads Stoumann, writing for CSS-Tricks, starts off with a simple idea: set a Custom Property to either playing or paused to control animation-play-state.

[data-animation] {
    /* … */
    animation-play-state: var(--animps, running);
}

/* Use a checkbox to pause animations */
[data-animation-pause]:checked ~ [data-animation] {
  --animps: paused;
}

But one of the listed use cases is great: a pure CSS slideshow that has a play/pause button:

See the Pen
<details> Play/Pause Animations
by Mads Stoumann (@stoumann)
on CodePen.

Nice one!

More use cases in the full post, including the use of an IntersectionObserver to pause animations for elements that are offscreen.

How to Play and Pause CSS Animations with CSS Custom Properties →

The future of CSS: Higher Level Custom Properties to control multiple declarations

When using CSS Custom Properties we mainly use them directly as variables in calculations for other properties. Having one CSS Custom Property control a varying set of other properties — such as both colors and numbers — is not exactly possible. There are some hacky workarounds we can use, but these don’t cover all scenarios. Thankfully there’s a new idea popping up: Higher Level Custom Properties. Although still premature, these Higher Level Custom Properties would allow us to drop the hacks.

Let’s take a look at our current options, and how this (possible) future addition to the CSS spec — along with the @if at-rule it introduces — might look …

~

~

# CSS Custom Properties as Variables

When working with CSS Custom Properties today, they are mainly used as CSS Variables. If you’ve used them, you’re quite familiar with code like this:

:root {
    --square-size: 2vw;
    --square-padding: 0.25vw;
}

.square {
    width: var(--square-size);
    padding: var(--square-padding);
    aspect-ratio: 1/1;
}

.square--big {
    --square-size: 16vw;
    --square-padding: 1vw;
}

Using the var() function we create a CSS Variable which gets substituted for the value of the Custom Property it refers to.

E.g. The variable var(--square-size) will hold the value of the --square-size Custom Property — namely 2vw — which is then set as the value for the width CSS property.

🤔 CSS Custom Properties vs. CSS Variables — Is there a difference?

Yes there's a difference:

  • A CSS Custom Property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo. Just like with a normal property you can assign a value to it, e.g. --foo: 200;.
  • A CSS Variable is created when the var() function is used. When creating the CSS Variable var(--my-prop), it will be replaced with the value of the --my-prop Custom Property it refers to, namely 200.

~

# Using CSS Custom Properties to affect multiple CSS declarations

In the example above we have two types of squares: regular sized ones and big ones. To differentiate between them we need to toggle the .square--big class. Toggling that class affects two CSS Custom Properties: both --square-size and --square-padding are altered.

But what if we wanted not to toggle a HTML class but a CSS Custom Property to do so? E.g. we want to toggle one CSS Custom Property, and have that automatically affect both --square-size and --square-padding.

As it stands today it’s not very straightforward to let one single CSS Custom Property affect multiple other CSS Properties, unless you resort to some hacky workarounds. Let’s take a look at the options we have today.

~

# Binary Custom Properties

If all you’re setting is numeric values, you can use Binary CSS Custom Properties within calculations. You give these Binary Custom Properties the value of 0 or 1 and use them within your calculations. Think of these Binary Custom Properties like light switches: they can either be OFF/false (0) or ON/true (1).

:root {
    --is-big: 0;
}

.square--big {
    --is-big: 1;
}

.square {
    width: calc(
        2vw * (1 - var(--is-big)) /* Value to apply when --is-big is 0 (~false) */
        +
        16vw * var(--is-big) /* Value to apply when --is-big is 1 (~true): */
    );
    padding: calc(
        0.25vw * (1 - var(--is-big)) /* Value to apply when --is-big is 0 (~false) */
        +
        1vw * var(--is-big) /* Value to apply when --is-big is 1 (~true): */
    );
    aspect-ratio: 1/1;
}

In the example above the --is-big Custom Property acts as a binary toggle that controls the results of the calc() functions. In the case of --is-big having a value of 0 those functions will yield one specific value, while when --is-big is set to 1 it will yield another value.

☝️ With some extra effort you can even perform Logical Operations (AND, NAND, OR, NOR, XOR, …) using CSS Custom Properties!?

Ana Tudor worked out the math for us in Logical Operations with CSS Custom Properties:

:root {
    --j: 1;
    --k: 0;
}

element {
    --notj: calc(1 - var(--j));
    --and: calc(var(--k)*var(--i));
    --nand: calc(1 - var(--k)*var(--i));
    --or: calc(1 - (1 - var(--k))*(1 - var(--i)));
    --nor: calc((1 - var(--k))*(1 - var(--i)));
    --xor: calc((var(--k) - var(--i))*(var(--k) - var(--i)));
}

🤯

~

# The Guaranteed-Invalid Value Hack

When you need to set things other than numeric values — such as colors — you can’t rely on a toggle that is either 0 or 1, as performing calculations with colors is invalid.

.square {
    /* ❌ This won't work! ❌ */
    color: calc(
        hotpink * (1 - var(--is-big))
        +
        lime * var(--is-big)
    );
}

The spec detailing calc() is clear on this:

It can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed.

CSS Values and Units Level 3: 8.1 Mathematical Expressions: `calc()`

What you can do however is use The CSS Custom Property Toggle Trick by James0x57 — which I like to call “The Guaranteed-Invalid Value Hack” — where you set a Custom Property to the “guaranteed-invalid value” of initial to force the var() function to use its fallback value:

If, for whatever reason, one wants to manually reset a variable to the guaranteed-invalid value, using the keyword initial will do this.

CSS Custom Properties for Cascading Variables Module Level 1: 2.2. Guaranteed-Invalid Values

In code it boils down to this:

--my-var: initial; /* initial => var() will use the fallback value */
color: var(--my-var, green); /* ~> green */
--my-var: hotpink; /* Any value other than `initial` (even simply one space!) => var() will not use the fallback value */
color: var(--my-var, green); /* ~> hotpink */

That means that you can flip the switch ON by setting a Custom Property to the value of initial. Here’s an example where the text will turn green and italic once --is-checked is flipped on:

input[type="checkbox"] + label {
    --is-checked: ; /* OFF */
    color: var(--is-checked, green);
    border: var(--is-checked, none);
    font-style: var(--is-checked, italic);
}

input[type="checkbox"]:checked + label {
    --is-checked: initial; /* ON */
}

A limitation of this approach however is that you can’t define several values to use in case --is-checked is in the OFF state. Say I want the text in the example above to be both red by default and with a border. Setting --is-checked to red will only get me halfway, as that value is only valid for the color property here.

input[type="checkbox"] + label {
    --is-checked: red; /* Default value to use */
    color: var(--is-checked, green); /* ✅ Will be red by default */
    border: var(--is-checked, none); /* ❌ What about a default value for border? */
    font-style: var(--is-checked, italic); /* ❌ What about a default value for font-style? */
}

~

# Update 2020.01.22: The Space Toggle Trick

UPDATE: As James0x57 himself pointed out in the comments below, the “CSS Custom Property Toggle Trick” can be used for this, but it takes some adjustments when compared to the implementation above. Here’s what James0x57 calls the Space Toggle Trick:

  • Consider the value   (space) to be the ON position, and the value of initial to be the OFF position.
  • Assign property values to new custom properties using the syntax --value-to-use-if-custom-toggle-is-on: var(--my-custom-toggle) value;, where you put the value to be used after the CSS Variable.

    --toggler: initial;
    --red-if-toggler: var(--toggler) red;
  • To use the value, use the var() syntax as before (e.g. adding a fallback value):

    background: var(--red-if-toggler, green); /* will be green! */
  • If you have more than one property than can affect a toggle, you can chain them up:

    • AND Logic:

      --red-if-togglersalltrue: var(--tog1) var(--tog2) var(--tog3) red;
    • OR Logic:

      -red-if-anytogglertrue: var(--tog1, var(--tog2, var(--tog3))) red;

Here’s a pen that applies his technique, with some cleaned up property names:

See the Pen
3. Binary Custom Properties + “The CSS Custom Property Toggle Trick” (Renamed)
by Bramus (@bramus)
on CodePen.

Thanks for clarifying James0x57, as I only understood half of your hack before 😅

~

# Future Solution: Higher Level Custom Properties

So the problem is that, as it stands today, we can’t have one single CSS Custom Property affect a varying set of other CSS Properties, or at least not in an easy way. At the CSS WG Telecon from early December 2020 Lea Verou proposed something called “Higher Level Custom Properties”, which would allow exactly that!

🚨 Do note that this proposal is still in it’s very very early stages and part of an ongoing discussion. The CSS WG has merely expressed interest in this proposal, suggesting that it should be explored further. If if tends to be helpful and possible, only then work on a Working Draft will start. Right now it still is a concept.

~

# Definition and Example

“Higher Level Custom Properties” are Custom Properties that control a number of other CSS Properties. As the proposal stands right now you use them in combination with a newly proposed @if at-rule, like so:

.square {
    width: 2vw;
    padding: 0.25vw;
    aspect-ratio: 1/1;

    @if (var(--size) = big) {
        width: 16vw;
        padding: 1vw;
    }
}

Unlike the Custom Properties we know today, a Higher Level Custom Property controls multiple declarations, way beyond simple variable substitution. In the example above we set our HLCP --size to have a value of big. This value isn’t used directly, but affects the other properties width and padding.

Using this HLCP also improves the meaning of our code. Setting width: 16vw; does not clearly express our intent, whereas setting --size: big; does.

💁‍♂️ If you don’t like @if then please don’t discard the whole idea immediately, but focus on the problem it’s trying to fix here. Lea’s proposal is a possible solution, not the solution. Could be that — in the end — we end up with a totally different syntax.

~

# Issues that still need to be tackled

Before you get too excited, there are still some cases that need to be taken care of. In a follow-up comment on the proposal, Lea documented some already identified issues.

🚨 Note that these issues are blocking issues. As long as these aren’t resolved, HLCPs won’t happen.

# Partial Application

A first issue is a problem with the desugaring of @if and partial application. Behind the scenes a @if at-rule desugars to the still discussed if() function call. The example above eventually becomes this:

.square {
    width: if(var(--size) = big, 16vw, 2vw);
    padding: if(var(--size) = big, 1vw, 0.25vw);
    aspect-ratio: 1/1;
}

This leads to no issue here, but it becomes quirky when comparing against percentages for example.

E.g. consider this:

.foo {
	@if (1em > 5%) {
		width: 400px;
		height: 300px;
	}
}

which desugars to:

.foo {
	width: if(1em > 5%, 400px);
	height: if(1em > 5%, 300px);
}

Now consider that an element that matches .foo is inside a 600px by 400px container and has a computed font-size of 25px; This makes 1em > 5% evaluate to false on the width property and true on the height property, which would make the @if partially applied. We most definitely don’t want that.

There are some ideas floating around to fix this — such as forcing percentages/lengths to always be compared against the width — but that’s still a bit vague right now.

# Cascading

Another issue that was pointed out is one on Cascading. I especially like this one, as it gives us a good insight in how CSS behaves and works:

Inline conditionals will have the IACVT (Invalid At Computed Value Time) behavior that we have come to know and love (?) from Custom Properties. Since @if will desugar to inline conditionals, it will also fall back to that, which may sometimes be surprising. This means that these two snippets are not equivalent:

.notice {
	background: palegoldenrod;
}

.notice {
	/* Desugars to background: if(var(--warning) = on, orange, unset); */
	@if (var(--warning) = on) {
		background: orange;
	}
}
.notice {
	/* Desugars to background: if(var(--warning) = on, orange, palegoldenrod); */
	background: palegoldenrod;

	@if (var(--warning) = on) {
		background: orange;
	}
}

You can file IACVT (Invalid At Computed Value Time) in the #TIL section there.

A declaration can be invalid at computed-value […] if it uses a valid custom property, but the property value, after substituting its var() functions, is invalid. When this happens, the computed value of the property is either the property’s inherited value or its initial value […].

This explains why in the example below the background won’t be red but (the default) transparent.

:root { --not-a-color: 20px; }
p { background-color: red; }
p { background-color: var(--not-a-color); }

👉 As 20px is no valid <color> value, the last declaration will become background-color: initial;.

💡 If we would have written background-color: 20px directly (e.g. without the use of Custom Properties), then that declaration would have simply been discarded due to being invalid, and we would have ended up with a red background.

~

# In Closing

The “Higher Level Custom Properties” idea by Lea Verou is one that quite excites me, as it solves an actual issue one can have in their code and would avoid having to use one of the nasty hacks.

There’s still a long way to go before we might actually see this land, yet as the CSS WG has expressed interest I’m hopeful that the already identified issues will be wrinkled out, and that work on an official spec can start.

If you have your own input on this subject, then I suggest to participate in the Higher Level Custom Properties discussion on GitHub.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

Injecting a JavaScript Attack Vector using CSS Custom Properties

Earlier this week I saw this tweet by Sansec float by:

This one’s pretty nice I must say: as the syntax for CSS Custom Properties is overly permissive (see here) you can use Custom Properties to store your JavaScript attack vector in. If you then use window.getComputedStyle to extract the contents of the Custom Property (see here) and combine it with a function constructor and an IIFE, it’s possible to execute it.

Here’s a pen that loads a remote confetti script using the method described:

Let this underline the importance of a Content Security Policy to prevent remote script loading script evaluation.

Update: Blocking this “hack” with a proper CSP

It took me some time to figure out — as I’m no CSP expert — but turns out the unsafe-inline keyword in the CSP’s source list is enough to block the execution of the JS-IN-CSS.

As a reminder, here are the four allowed keywords:

  • 'none', as you might expect, matches nothing.
  • 'self' matches the current origin, but not its subdomains.
  • 'unsafe-inline' allows inline JavaScript and CSS.
  • 'unsafe-eval' allows text-to-JavaScript mechanisms like eval.

I first thought unsafe-inline would be insufficient here as the code does not call eval, but apparently a function constructor is (correctly!) considered equally harmful, and therefore also blocked.

Here’s an updated demo that blocks the script evaluation:

See the Pen
Injecting a JavaScript attack vector using CSS Custom Properties (with CSP)
by Bramus (@bramus)
on CodePen.

The CSP used is this one:

<meta
    http-equiv="Content-Security-Policy"
    content="script-src https://cpwebassets.codepen.io https://cdpn.io https://cdn.jsdelivr.net 'unsafe-inline';"
>

It works as follows:

  • https://cpwebassets.codepen.io and https://cdpn.io are there for the CodePen demo to work
  • https://cdn.jsdelivr.net is there to allow legitimate loading of scripts — such as a jQuery you might need — from that CDN.
  • unsafe-inline is the one that prevents the execution of the JS-IN-CSS defined script by blocking the call to the function constructor

That calls for confetti! 🤪

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

What values can you put in a CSS Custom Property?

Will Boyd:

CSS custom properties can hold all sorts of things. Some of these things were not obvious to me, which is why I decided to write this.

In short: they can contain just about everything. It’s only until CSS Custom Properties are used in a certain context as a CSS Variable — using var() — that they are evaluated.

☝️ To understand the above you should know that CSS Custom Properties are not Variables

A CSS Custom Property allows you to define a property with a certain value, e.g. --width: 200;.

It’s only when it’s used with var() — e.g. var(--width) — that you are creating a CSS Variable to use.

That’s why you can use CSS Custom Properties to:

What Can You Put in a CSS Variable? →

CSS Custom Properties are not Variables

When mentioning CSS Custom Properties here on bram.us I do tend to name them like that — and not CSS Variables — as that’s their official name.

I always thought the terms could be used interchangeably — with CSS Variables simply being the unofficial name — but as detailed by Šime Vidas on Web Platform News that’s not the case:

The spec distinguishes the two terms: A custom property is not a variable, but it defines a variable. Any property can use variables with the var() function whose values are defined by their associated custom properties.

Aha!

CSS custom properties are not variables →