CSS light-dark() is a game changer
It was never that simple to set up light and dark mode in web-applications. With the new CSS light-dark-function, we have a really good addition to simplify the handling of the color scheme.

There are several ways to set up your web-application for light and dark mode. But it does make a difference, depending on your requirements. For example the modes could only change according to the preference given by the system or browser settings. The mode could be changeable through a button on the application. Or even both.
Let's have a quick look on to it.
Light and Dark Mode with preferred system theme
We could setup our base color palette with CSS variables for light mode and then use a media query to change them if the user settings prefers the dark mode:
:root {
--bg-color-100: #ffffff;
--bg-color-200: #e6e6e6;
--bg-color-300: #cccccc;
--bg-color-400: #b3b3b3;
--bg-color-500: #999999;
--bg-color-600: #808080;
--bg-color-700: #666666;
--bg-color-800: #4d4d4d;
--bg-color-900: #333333;
@media (prefers-color-scheme: dark) {
--bg-color-100: #1a1a1a;
--bg-color-200: #333333;
--bg-color-300: #4d4d4d;
--bg-color-400: #666666;
--bg-color-500: #808080;
--bg-color-600: #999999;
--bg-color-700: #b3b3b3;
--bg-color-800: #cccccc;
--bg-color-900: #e6e6e6;
}
}
With the new CSS light-dark() function, this could be combined like so:
:root {
--bg-color-100: light-dark(#ffffff, #1a1a1a);
--bg-color-200: light-dark(#e6e6e6, #333333);
--bg-color-300: light-dark(#cccccc, #4d4d4d);
--bg-color-400: light-dark(#b3b3b3, #666666);
--bg-color-500: light-dark(#999999, #808080);
--bg-color-600: light-dark(#808080, #999999);
--bg-color-700: light-dark(#666666, #b3b3b3);
--bg-color-800: light-dark(#4d4d4d, #cccccc);
--bg-color-900: light-dark(#333333, #e6e6e6);
}
It works the same and is much shorter, but that is not the only reason for calling that a game changer.
Light and Dark Mode with a button
If you want to have a solution with a button, you have to work with a class.
You cannot use media queries with that approach.
The class could for example be attached to the body
, which inherits changes in style to every child element.
<!doctype html>
<html lang="de">
<head>
<!-- ...... -->
</head>
<body class="dark">
<button id="theme-change-btn">Light/Dark</button>
<h1>Some awesome Heading</h1>
</body>
</html>
For the dark colors can select that class and change the variables:
body.dark {
--bg-color-100: #1a1a1a;
--bg-color-200: #333333;
--bg-color-300: #4d4d4d;
--bg-color-400: #666666;
--bg-color-500: #808080;
--bg-color-600: #999999;
--bg-color-700: #b3b3b3;
--bg-color-800: #cccccc;
--bg-color-900: #e6e6e6;
}
Then we can have a button with some JavaScript, that toggles that class:
document.querySelector('#theme-change-btn')
.addEventListener(('click', (_event) => {
document.body.classList.toggle('dark');
};
But how about when you want to have both functionalities at once?
Light and Dark Mode combined options
In that case you have to set it up like in the previous example with the button.
You can check for the preferred color scheme in JavaScript using window.matchMedia('(prefers-color-scheme: dark)').matches
and set the dark
class initially based on that. Then you can simply change it like before with the button.
This would be a small example how you could do that in React & Tailwind:
import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import Header from './components/Header';
import Footer from './components/Footer';
const getMediaColorScheme = () => {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
};
function App() {
const [darkTheme, setDarkTheme] = useState(getMediaColorScheme);
// Change via Button
const toggleTheme = () => {
setDarkTheme((currentState) => !currentState)
};
// Change when system changes
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)');
colorSchemeQueryList.addEventListener('change', (event) => {
setDarkTheme(event.matches);
});
useEffect(()=> {
const body = document.querySelector('body');
darkTheme ? body.classList.add('dark') : body.classList.remove('dark')
}, [darkTheme])
return (
<div className={ (darkTheme ? 'dark ' : '') + 'main'}>
<Header toggleTheme={toggleTheme} />
<main className='bg-gray-200 dark:bg-gray-800'>
<Outlet/>
</main>
<Footer />
</div>
)
}
export default App
However it can be done even simpler than that! Here is how...
Light and Dark Mode combined with CSS light-dark()
There is a slightly difference between the solution with media queries @media (prefers-color-scheme: dark)
and light-dark()
. CSS light-dark() detects not only the users preferred color scheme, which is given by the system/browser settings. It also detects if the developer has set a light or dark color scheme.
The initial approach with the media query can be combined like so:
:root {
color-scheme: 'light dark';
--bg-color-100: light-dark(#ffffff, #1a1a1a);
--bg-color-200: light-dark(#e6e6e6, #333333);
--bg-color-300: light-dark(#cccccc, #4d4d4d);
--bg-color-400: light-dark(#b3b3b3, #666666);
--bg-color-500: light-dark(#999999, #808080);
--bg-color-600: light-dark(#808080, #999999);
--bg-color-700: light-dark(#666666, #b3b3b3);
--bg-color-800: light-dark(#4d4d4d, #cccccc);
--bg-color-900: light-dark(#333333, #e6e6e6);
}
It works the same and is much shorter, but that is not the only reason for calling that a game changer.
Notice the color-scheme
option. This option defines that this application can be used for light
and dark
. You can set it strictly to only light
or dark
and the application will only be available in that setting. And that is basically the trick!
We can change the color scheme (of the root
element) using: document.documentElement.style.setProperty('color-scheme', 'dark')
Eventually we want to check the preferred color scheme initially and at the end we can end up with a function like this which can be controlled by a button.
const initialTheme =
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
let currentTheme = initialTheme ? 'dark' : 'light'
const root = document.documentElement
function handleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light'
root.style.setProperty('color-scheme', currentTheme)
}
Conclusion
With the new light-dark()
function, we have a really good addition to simplify the handling of the color scheme. In my opinion the last approach is much less complicated.
I always wished that the mode could simply be switched without creating something around the given concept.
Of course in case you want to have multiple themes besides dark and light, you will eventually end up in a solution with multiple classes.