This content originally appeared on Bram.us and was authored by Bramus!
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.
See the Pen
Custom CSS Custom Functions: –light-dark() by Bramus (@bramus)
on CodePen.
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. When set to system
.
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);
}
/* If the container is set to "system" and the system is set to "dark", use the --dark value */
@container style(--scheme: system) {
@media (prefers-color-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 value is also duplicated into a --root-scheme
for further use.
: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 I chose to use these elements as extra wrapper elements, I 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>
Here’s a live demo. Remember that you need Chrome Canary with the Experimental Web Platform Features Flag to see the code in action.
See the Pen
Custom CSS Custom Functions + Nested Style Queries (+ attr()): –light-dark() by Bramus (@bramus)
on CodePen.
~
# 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. With 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.
The if()
function also accepts style queries, so 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));
}
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;
}
Here’s a live demo to check out:
See the Pen
Custom CSS Custom Functions + Nested inline if() (+ attr()): –light-dark() by Bramus (@bramus)
on CodePen.
~
# 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:
This content originally appeared on Bram.us and was authored by Bramus!

Bramus! | Sciencx (2025-02-18T22:40:50+00:00) CSS @function + CSS if() = 🤯. Retrieved from https://www.scien.cx/2025/02/18/css-function-css-if-%f0%9f%a4%af/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.