In https://brm.us/css-custom-functions I took a first look at Chrome’s prototype of Custom Functions (CSS @function
). Since then the prototype in Chrome got updated with nested container queries support and CSS if()
also got added … and like I said: it’s a game changer
~
⚠️ This post is about an upcoming CSS feature. You can’t use it … yet.
This feature is currently being prototyped in Chrome Canary and can be tested in Chrome Canary with the Experimental Web Platform Features flag enabled.
~
# The quest for a light-dark()
that works with any value.
The function I built in https://brm.us/css-custom-functions is a custom --light-dark()
that can be used to return values depending on whether light or dark mode is being used.
@function --light-dark(--light, --dark) { result: var(--light);
@media (prefers-color-scheme: dark) { result: var(--dark); } }
Unlike the built-in light-dark()
, this custom function is not limited to <color>
values and works with any type of value. But also unlike light-dark()
it cannot respond to the local color-scheme
value and can only respond to the light/dark media preference.
As hinted at the end of the post, this limitation can be removed once support for nested container queries and/or CSS if()
got added to Chrome … and that day has come!
~
# A custom --light-dark()
using Container Queries
ℹ️ Because this code uses container queries you always need a wrapper element. The next section that uses if()
does not need this extra wrapper element.
Since my previous post the prototype in Chrome got expanded to also support nested container queries inside custom functions. This opens the path to allowing a per-element light/dark preference, like so:
- Set a preferred color-scheme on an element using a custom property named
--scheme
- Rework the
--light-dark()
to use a style query to respond to the value of--scheme
The possible values for --scheme
are light
, dark
, and system
. When --scheme
is set to one of the first two, the color-scheme
is forced to that value.
The function looks like this:
@function --light-dark(--light, --dark) { /* Default to the --light value */ result: var(--light);
/* If the container is set to "dark", use the --dark value */ @container style(--scheme: dark) { result: var(--dark); } }
Inside the @function
, the --light
and --dark
values are passed in as arguments to the function. The --scheme
custom property however is read from the element on which the function is invoked.
To ensure that there is some value for --scheme
, I set it on the :root
depending on the prefers-color-scheme
value. The prefers-color-scheme
value is also duplicated into a --root-scheme
to enable support for a --scheme
value of system
, but more on that later.
:root { --root-scheme: light; --scheme: light;
@media (prefers-color-scheme: dark) { --root-scheme: dark; --scheme: dark; } }
To allow setting a preferred color scheme on a per-element basis, I resorted to using a data-scheme
HTML attribute which I parse to a value in CSS using attr()
. When the value is light
or dark
I use the value directly. When the value is system
, the code uses the --root-scheme
property value. To play nice with nested light/dark contexts the code uses @scope
.
/* Allow overriding the --scheme from the data-scheme HTML attribute */ @scope ([data-scheme]) { /* Get the value from the attribute */ :scope { --scheme: attr(data-scheme type(<custom-ident>)); }
/* When set to system, use the --root-scheme value (which is determined by the MQ) */ :scope[data-scheme="system"] { --scheme: var(--root-scheme); }
/* This allows the native light-dark() to work as well */ :scope > * { color-scheme: var(--scheme); }
/* Because the elements with the attribute are extra wrapper elements, we can just display its contents */ display: contents; }
To learn about this attr()
, go read CSS attr()
gets an upgrade. As for @scope
, it’s sufficient to read the quick intro on @scope
.
With all pieces in place it’s time to use it.
In CSS:
[data-scheme] > * { color: light-dark(#333, #e4e4e4); background-color: light-dark(aliceblue, #333);
border: 4px --light-dark(dashed, dotted) currentcolor; font-weight: --light-dark(500, 300); font-size: --light-dark(16px, 18px);
transition: all 0.25s ease, border-style 0.25s allow-discrete; }
In HTML:
<div data-scheme="light">
<div class="stylable-thing">
…
</div>
</div>
Here’s a live demo. Remember that you need Chrome Canary with the Experimental Web Platform Features Flag to see the code in action.
~
# A custom --light-dark()
using Inline if()
As of Chrome Canary 135.0.7022.0 the inline if()
is also available behind the Experimental Web Platform Features flag. Thanks to this function you can omit the extra container element that the container queries approach needs, as you can conditionally select a value directly in a declaration.
Because the if()
function also accepts style queries as one of the conditions, the overall approach remains the same: use a custom property and respond to its value. The resulting code however is much much shorter:
@function --light-dark(--light, --dark) {
result: if(
style(--scheme: dark): var(--dark);
else: var(--light)
);
}
Side note: Did you know inline if()
can accept multiple conditions? Like so:
color: if(
style(--scheme: dark): var(--dark-color);
style(--scheme: dim): var(--dim-color);
else: var(--light)
);
That type of usage is beyond the scope of this post, though.
The code to set --scheme
to light
or dark
also is shorter, as it’s more easy to fall back to the --root-scheme
value.
:root { --root-scheme: light; --scheme: light;
@media (prefers-color-scheme: dark) { --root-scheme: dark; --scheme: dark; } }
@scope ([data-scheme]) { :scope { --scheme-from-attr: attr(data-scheme type(<custom-ident>)); --scheme: if( style(--scheme-from-attr: system): var(--root-scheme); else: var(--scheme-from-attr) ); color-scheme: var(--scheme); /* To make the native light-dark() work */ } }
Usage remains the same as before, with the difference that you can set the color-scheme dependent styles directly on the [data-scheme]
element.
[data-scheme] { color: light-dark(#333, #e4e4e4); background-color: light-dark(aliceblue, #333);
border: 4px --light-dark(dashed, dotted) currentcolor; font-weight: --light-dark(500, 300); font-size: --light-dark(16px, 18px);
transition: all 0.25s ease, border-style 0.25s allow-discrete; }
<div class="stylable-thing" data-scheme="light">
…
</div>
Here’s a live demo to check out:
~
# Conclusion
I was already very much excited about CSS Custom Functions by itself. Combining it with inline if()
takes that to even a higher level.
Expressed through the Galaxy Brain (aka Expanding Brain) meme, this is how I feel about this:

~
# Spread the word
Feel free to reshare one of the following posts on social media to help spread the word:
~
🔥 Like what you see? Want to stay in the loop? Here's how: