Debug panel

Close debug panel
Roma’s Unpolished Posts

Functional Capturing

Published on:
Categories:
CSS Functions 2, CSS 68
Current music:
mudy on the sakuban — New Theory
Current drink:
Jasmine Green Tea

The Context

Chrome currently has an experimental implementation of custom CSS functions in its Canary version and also recently shipped if() conditions in stable Chrome 137.

In February, I wrote a quick post about them: Intent to Experiment for Longer — more specificlaly, about how it is better to wait before shipping the features without sufficient developer feedback.

One thing I forgot to mention in that post: the features, as far as I tested them, worked pretty well, and I generally liked their design. My main blocker that was there for functions — a necessity to explicitly define dependencies for functions — was resolved to be removed (official resolution: “Kill using with fire”), and, with it, I did not yet find any other personal blockers, for now.

That said: even when the design and implementation are sound, with the Web Platform it is always better to be on a more cautious side, especially with foundational features. And while I did not yet find any other blocking issues, this does not mean they do not exist. I would be happy if there are none! But if you’re a web developer: please, play with these new features, and give your feedback.

In a small series of blog posts, I’ll write about a few things I found interesting in the current prototype implementations of functions and conditions.

This post is about one feature of custom functions that I will likely use all the time, but which might not be obvious at the first glance.

Basic Capturing

Before continuing, I recommend reading my Captured Custom Properties post, in which I talk a bit about the registered custom properties, some use cases for them, and a pattern I currently use with them.

With custom functions, some of those use cases could be solved much more elegantly!

Let’s look at one example from my “Captured Custom Properties” post.

Registered Custom Property Example

In this example, we can use a custom property to “evaluate” its value on the element, so it will store the value of 1.5em in pixels, so it could be used on its children through inheritance.

Outer: Foo
Inner: Bar
@property --registered-font-size {
	syntax: "<length>";
	initial-value: 0px;
	inherits: true;
}

.example-reg {
	font-size: 0.666em;
	--registered-font-size: 1.5em;

	& span {
		font-size: var(--registered-font-size);
	}
	& .inner {
		font-size: 2em;
	}
}
Storing the font-size on the root, and restoring it on an inner element.

This is great, and while my pattern of having a dedicated suit of registered custom properties works well, we have to register new custom properties whenever we want to use multiple of them on the same element.

Even though you cannot register a custom property from inside a custom function, one of their features works similarly, and we can use it to simplify things a lot.

Custom Function Example

Outer: Foo
Inner: Bar
@function --capture-length(--value <length>) {
	result: var(--value);
}

.example-fun {
	font-size: 0.666em;
	--captured-font-size: --capture-length(1.5em);

	& span {
		font-size: var(--captured-font-size);
	}
	& .inner {
		font-size: 2em;
	}
}
Storing the font-size on the root, and restoring it, now via a custom function.

This works the same! But now we have a reusable --capture-length() function, and with its parameter being typed, this parameter behaves like a registered custom property — capturing its value, and passing the evaluated value, and not the tokens in abstract.

The spec text for the custom function evaluation explicitly describes how exactly those parameters are registered:

For each function parameter of custom function, create a custom property registration with the parameter’s name, a syntax of the parameter type, an inherit flag of “true”, and no initial value. Add the registration to registrations.

Interestingly, if we define a return type for a function, it will also be “captured”, so those two are identical:

@function --capture-length(--value <length>) {
	result: var(--value);
}
@function --capture-length(--value) returns <length> {
	result: var(--value);
}

But using the type for a parameter is just so slightly more compact.

Use Cases for Capturing Length

I won’t repeat everything I wrote in my original article about it, but to sum up some common use cases:

Universal Capturing

The function above only captured a single type — <length>. But we can make it capture multiple different types at the same time!

@function --capture(
	--value type(<length>|<number>|<color>)
) {
	result: var(--value);
}

This will work just as well!

More Use Cases

However, the use cases for things outside <length> are rare.

One example for <color> I also gave in my previous post on topic: Unlocking Style Queries.

Until now, if we wanted to check the value of some color, like inside a style query, we had to “normalize” it and use a registered custom property for this.

We can do this with our --capture function as well — and! — with conditions in CSS, we could implement a --light-dark() custom function similar to what I described in my Querying the Color Scheme post, but one that will work without the downside of style queries!

Here is a heavily modified example from the Single Source of Truth section of that post, but now using the custom functions and if():

I should be always light. Current scheme: lightdark.

I should be always dark. Current scheme: lightdark.

@function --capture(
	--value,
	--_value type(<length>|<number>|<color>):
		var(--value)
) {
	result: var(--_value);
}

@function --light-dark(--a, --b) {
	--captured-color:
		--capture(light-dark(white, black));

	result: if(
		style(--captured-color: --capture(#FFF)):
			var(--a);
		else:
			var(--b);
	);
}

.example--light {
	color-scheme: light;
}

.example--dark {
	color-scheme: dark;
}

.example {
	padding: 1em;
	background: Canvas;
	color: CanvasText;
	border:
		2px
		--light-dark(solid, dashed)
		--light-dark(hotpink, pink);

	@layer {
		.dark-only {
			display: --light-dark(none, revert-layer);
		}

		.light-only {
			display: --light-dark(revert-layer, none);
		}
	}
}

We can see that our --light-dark() function works for any values — not just colors — and works perfectly on the same element on which we define the color-scheme!

There are a few interesting things going on that demand additional explanations.

Capturing light-dark()

You could notice that the CSS of the --capture function here is different from what I proposed initially: it has two arguments, with the second one having its value assigned from the first one by default.

What is this, and why? The reason: light-dark() works only for color contexts. It might be Chrome’s implementation bug, but just passing light-dark (…) to a custom function, even if the argument is typed, will not capture it.

But by doing this gambit, where we reassign that argument to another typed argument, we can work around this!

revert-layer, for now

I’m using a revert-layer and a custom cascade layer around the if()s that touch display property, but in the future we could use revert-rule for this.

If you don’t know what revert-rule is — in January CSSWG resolved to add it to CSS, following New !revertable flag to mark a declaration as ‘can be reverted when IACVT’ by Lea Verou, and an older proposal Proposal: additional CSS-Wide Keyword, ‘ignore’ (primarily for css variable fallbacks) by Jane Ori.

This CSS-wide keyword will be invaluable for any conditional styles!

if() evaluation

While the if() did ship in stable Chrome 137, it is not yet in its final form. Currently, you have to jump through a few hoops when you want to compare a return value of some function: assign it to another custom property, and also make sure the types match inside style() — style(--captured-color: --capture(#FFF)) — here we have to explicitly call our --capture() around the #FFF to make sure it will be properly compared with the --captured-color.

This is a known “gotcha”: for example, Temani Afif wrote a How to correctly use if() in CSS post about this, in which he points that we can use registered custom properties to fix this. Our --capture() function might be an even better solution to this!

And, in the future, the way style() is evaluated might change, so there is a chance we could simplify this place later.

Color Capturing Limitations

When exploring capturing the <color> type for another article, I discovered that it does not work for any colors that include currentColor.

I won’t repeat myself, but I welcome you to check the Problem of the Current Color section of my Passing Data into SVG: Linked Parameters Workaround article (which I also recommend reading in full — it contains many fun bits!)

I imagine there might be other similar cases, where we couldn’t capture something that evaluates at used time, and not computed which both registered custom properties and typed function arguments care about.

Fun Future

I can’t wait for what else we could do with custom functions, if(), revert-rule, and many other incoming features which will make the foundation of future CSS.

At the first glance, functions might seem to be just a syntax sugar over something you could otherwise achieve with preprocessors. But, their type capabilities, and the way they interact with everything else in CSS, allows us to abstract things in much more dynamic ways than previously possible in static CSS.

If you want to play with that future: I recommend trying Chrome Canary with its Experimental Web Platform Features flag, and give your feedback to CSS Working Group and Chromium. And me — I would love to look at your experiments as well!

Please share your thoughts about this on Mastodon!