To change a color based on whether Light Mode or Dark Mode used, you’d typically use a prefers-color-scheme
Media Query. To make things easier, CSS now comes with a utility function named light-dark()
. The function accepts two color values as its arguments. Based on which color scheme you are actively using, it will output the first or the second argument.
~
# Responding to Light or Dark Mode
To change a color value – or any other value for that matter – based on Light Mode or Dark Mode being used, you’d typically use a prefers-color-scheme
Media Query to change the value of a Custom Property:
:root {
--text-color: #333; /* Value for Light Mode */
}
@media (prefers-color-scheme: dark) {
:root {
--text-color: #ccc; /* Value for Dark Mode */
}
}
When implementing Dark Mode, you typically end up with a bunch of duplicated CSS variables that set the values for each mode. The rest of your CSS then uses these custom properties for the actual declarations.
body {
color: var(--text-color);
}
~
# Responding to Light or Dark Mode with light-dark()
A new addition to the CSS Color Module Level 5 Specification is the light-dark()
function. The function accepts two color values as its arguments. Based on which color scheme you are actively using, it will output the first or the second color argument.
light-dark(<color>, <color>);
As per spec:
This function computes to the computed value of the first color, if the used color scheme is
light
or unknown, or to the computed value of the second color, if the used color scheme isdark
.
The used color scheme is not only based on the user’s Light/Dark Mode setting, but also on the value of the color-scheme
property. This similar to how System Colors get computed.
The
color-scheme
property allows an element to indicate which color schemes it is designed to be rendered with. These values are negotiated with the user’s preferences, resulting in a used color scheme […].
That means, for light-dark()
to work, you must also include a color-scheme
declaration.
:root {
color-scheme: light dark;
}
:root {
--text-color: light-dark(#333, #ccc); /* In Light Mode = return 1st value. In Dark Mode = return 2nd value. */
}
Because color-scheme
is taken into account, that also means that you can override its value per element, to force it into a certain mode:
.dark {
color-scheme: dark; /* light-dark() on this element and its children will always return dark */
}
🤔 If this light-dark()
seems familiar: Chromium internally sports a -internal-light-dark()
which I wrote about before. Based on this functionality, the proposal was made within the CSS Working Group to expose a similar function to authors. The result is light-dark()
.
Unlike -internal-light-dark()
which is for any type of value, light-dark()
can only be used for colors.
~
# What about other non-<color>
values and responding to other color schemes?
A common type of feedback on light-dark()
I get is that it is fairly limited in what it can do: it can only do light/dark and only works with <color>
values. That’s correct and is also very much intentional, because it is an intermediary step towards a final solution.
As proposed in the CSS Working Group issue, the end goal is to have a function (tentatively) named schemed-value()
in the future. That function can:
- Respond to any value of
color-scheme
. - Return more than
<color>
values
It could look something like this:
:root {
color-scheme: dark light custom;
}
body {
color: schemed-value(
light hotpink, /* Value to use when the light color-scheme is used */
dark lime, /* Value to use when the dark color-scheme is used */
--custom rebeccapurple /* Value to use when the --custom color-scheme is used */
);
}
But, for now, we “only” have light-dark()
and I personally think that’s fine, as it rhymes with today’s reality of what browsers can do:
light-dark()
only works withlight
ordark
because right nowcolor-scheme
only acceptslight
ordark
(ornormal
) as keywords. Custom values are currently ignored, so it is of no use to build a generic function today.- It can only do
<color>
values because the parser needs to know the value type of what it is parsing ahead of time. To cater for this,light-dark()
is explicitly defined to be a<color>
, allowing it be used anywhere a<color>
is expected – e.g. thebackground-color
andcolor
properties.
Narrowing things down in feature scope – from the very broad schemed-value()
to the slimmed down light-dark()
– allowed the function as it is to be defined today, instead of putting the whole thing on the long track.
The name and syntax of light-dark()
is very memorable, easy to use, and – most importantly – offers a solution to a common use-case authors are having today.
# Looking ahead
💡 Once the day comes when authors are able to create their own custom color-scheme
s, only then the much broader schemed-value()
would become useful.
It’s still only a proposal, but as it stands right now you would register a custom color scheme using a name, a base-scheme to start from, and then specifying the values for the System Colors:
/* Fully fledged custom color scheme */
@color-scheme --solarized-dark {
base-scheme: dark;
AccentColor: …;
AccentColorText: …;
…
}
Once registered, this custom scheme becomes a valid value for color-scheme
, schemed-value()
and – thanks to the Web Preferences API – in prefers-color-scheme
Media Query Conditions.
.code-editor {
color-scheme: --solarized-dark;
}
The reason that color-scheme
property will be able to accept these custom values, is because the CSS Working Group has future-proofed it. Today, the syntax already accepts <custom-ident>
s but currently ignores them:
<custom-ident>
values are meaningless, and exist only for future compatibility, so that future added color schemes do not invalidate thecolor-scheme
declaration in legacy user agents. User agents must not interpret any<custom-ident>
values as having a meaning; any additional recognized color schemes must be explicitly added to this property’s grammar.
Also, when schemed-value()
ever becomes a thing, light-dark()
would become syntactic sugar for it:
light-dark(<color>, <color>); = schemed-value(light <color>, dark <color>);
Pretty sweet, right?
~
# Browser Support
💡 Although this post was originally published in October 2023, the section below is constantly being updated. Last update: Feb 1, 2024.
Here is an up-to-date list of browser support for CSS light-dark()
:
- Chromium (Blink)
-
✅ Included in Chrome 123.0.6273.0, which goes stable on Mar 13, 2024
- Firefox (Gecko)
-
✅ Supported in Firefox 120.
- Safari (WebKit)
-
⏳ Feature landed in WebKit on main. Expected to be included in Safari TP 188.
The pen embedded below will indicate if the browser you are currently using supports CSS light-dark()
or not:
See the Pen
CSS light-dark() Support test by Bramus (@bramus)
on CodePen.
To stay up-to-date regarding browser support, you can follow these tracking issues:
- Chromium/Blink: Issue #1490618 — Fixed (Closed)
- Firefox/Gecko: Issue #1856999 — RESOLVED FIXED
- Safari/WebKit: Issue #262914 — RESOLVED FIXED
~
# Demo
If your browser supports light-dark()
, the demo below will show a few <div>
s labeled .auto
that respond to Light/Dark mode being toggled. The <div>
s with the class .light
or .dark
are forced into their proper mode.
See the Pen
light-dark() Demo by Bramus (@bramus)
on CodePen.
~
# Spread the word
To help spread the contents of this post, feel free to retweet its announcement tweetpost/tootpost:
To change a color based on Light Mode or Dark Mode, you’d typically use a `prefers-color-scheme` Media Query.
To make things easier, CSS now comes with a `light-dark()` utility function.
Read https://t.co/uzcTGPo8dY to get to know the details.
Browser Support: Firefox 120. pic.twitter.com/1rmGkKy2yl
— Bramus (@bramus) October 9, 2023
~
🔥 Like what you see? Want to stay in the loop? Here's how:
Leave a comment