Allowing users to switch between light and dark colour schemes on websites is a popular feature. You can often see a small sun or moon icon that allows you to change from light mode to dark colour scheme, or visa versa. Some operating systems and user agents also have the ability to activate a dark colour scheme, which websites also pick up and use.
This feature is so popular, in fact, that a number of browser extensions exist to allow users to force websites to be shown in certain colour schemes, the most popular ones making the site dark. Having a dark colour scheme on a website aids in readability, especially for people who have ADHD and similar disorders.
From a person perspective, having a dark colour scheme in a website means that I can view the content in the middle of the night without causing myself eye strain or disturbing anyone else. I often have trouble sleeping so the ability to read articles in the dark has become quite important to me.
I was researching using colour schemes this week as part of converting a website to have a light and dark colour scheme switcher. It turns out that adding dark mode to CSS themes is pretty simple, but it does take a little bit of planning and forethought to get working in a maintainable way.
In this article we will look at what colour schemes are, how to trigger them and detect them, and how to organise your theme to best take advantage of them.
You can skip to the relevant section using this menu.
The color-scheme property is used to inform an element of the colour scheme available to it. This is our primary property when setting the colour scheme of the page (hence the name) and can be updated by the operating system and user agent to select a different colour scheme for the page.
We can set this property to one of the following options.
normal - This is the default setting for this property. The element will use the colours set by the page's colour scheme (set in the metadata of the page). If no colour scheme is available then the colours will be taken from the style sheets of the page.light - The element will be rendered using the light colour scheme.dark - The element will be rendered using the dark colour scheme.only - This setting prevents the user agent from overriding the colour scheme for an element by setting either only light or only dark. We can use this to prevent an element on a page from changing colour based on the colour scheme settings of the page.
We can also combine the light and dark values into a single option and create a preference between light and dark colour schemes. For example, setting the value to light dark means that the page should use the light colour scheme first, and fall back to the dark colour scheme if light is not available.
p {
color-scheme: light dark;
}
When selecting a colour scheme, the user agent will tend to also change the following aspects of the page to match the available colour scheme.
- The background of the canvas, set to white normal or light mode, and black for dark mode.
- The default colour for UI elements in the browser, like scrollbars.
- The default colour for form elements.
- The default colours of other elements on the page that are provided by the browser. This include spell check underlines, selection colours, missing image icons, that sort of thing.
This value is defined by the operating system or the settings of the user agent.
The best thing to do if you want to set a global colour scheme is to set this property in the :root element of your styles.
:root {
color-scheme: light dark;
}
By setting this in the :root element we are telling the entire page that every element has a light and a dark colour scheme. Not only that, we essentially let the operating system select what colour scheme to select for the entire page.
The color-scheme property is widely accepted by all browsers, including mobile browsers, so it is safe to use this as the core principle of your site colour scheme.
In addition to adding this to your CSS it is also a good idea to set the <meta name="color-scheme"> meta tag in the <head> tag of the page as this will prevent the page flashing after the styles have been loaded in and the colour scheme information is selected.
<meta name="color-scheme" content="dark light" />
Setting this property in your styles will probably change some of the colours on your page, so let's look at detecting colour schemes and changing colours of elements based on that information.
Now that we have established that our theme uses a colours scheme we need a way of detecting it so that we can adapt our theme to the correct scheme. This is done using the prefers-color-scheme media query, which we can stipulate what the scheme is we want to detect.
For example, if we want to apply some custom styles for the dark colour scheme then we can do the following.
@media (prefers-color-scheme: dark) {
}
The opposite of this is, of course, to set the light theme.
@media (prefers-color-scheme: light) {
}
You won't always need to use both of these though. The basic idea is that if you set your color-scheme property to light dark then the "light" scheme becomes your default and you then override the colours you set in your theme using the "dark" media query.
There are a couple of options open to us to pick this or that colour for different elements.
The simplest thing we can do here is to set up some colours as the light colour scheme, and then swap to the dark colour scheme using the prefers-color-scheme media query. In the following example we set the text colour of a paragraph tag to white and the background to black, and then reverse this in the media query.
p {
color: #000;
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
p {
color: #fff;
background-color: #000;
}
}
We can also use the light-dark() function that is built into CSS and gives us the ability to select the appropriate colour without needing to use media queries to detect the colour scheme in use. The function takes two parameters, which is the light colour, followed by the dark colour.
This function is only available to us if we first set the value of the color-scheme property to "light dark", after which we can then allow CSS to pick the correct colour for us from the two options.
The following example shows the use of the light-dark() function to select the appropriate colour for the text and background of a paragraph tag.
:root {
color-scheme: light dark;
}
p {
color: light-dark(#000, #fff);
background-color: light-dark(#fff, #000);
}
Rather than having to copy and paste this value all over your site it's probably a good idea to abstract these values into CSS variables. We can start this off by abstracting the individual colours.
In the example below, we are naming the variable based on the colour scheme it should be part of (or just normal if no colour scheme is selected).
:root {
--text: #000;
--background: #fff;
--text-dark: #fff;
--background-dark: #000;
}
p {
color: light-dark(var(--text), var(--text-dark));
background-color: light-dark(var(--background), var(--background-dark));
}
An alternate approach to this is to abstract the result of the light-dark() function into a variable and pass that to the property in question. Doing this allows us to change the colour in just one place, rather than hunting for all instances of that colour in light-dark() function calls.
:root {
--text: #000;
--background: #fff;
--text-dark: #fff;
--background-dark: #000;
--light-dark-text: light-dark(var(--text), var(--text-dark));
--light-dark-background: light-dark(var(--background), var(--background-dark));
}
p {
color: var(--light-dark-text);
background-color: var(light-dark-background);
}
As you can see from these examples, but adding just a few lines of CSS we have allowed a colour scheme preference to be set from a operating system or user agent level. This means that if the user agent is set to be in dark mode then the user will automatically be shown dark colours from the site, without them having to change or select anything.
Allowing the user to select their preference for the site is addressed later in the article.
The use of images can be a bit tricky with colour schemes. If you upload an image for the light colour scheme then this will seem very bright on the dark colour scheme as the image will be surrounded by dark colours and stand out more. There are a few approaches to this, but let's look at a couple that you might want to employ.
The first thing we can try is to apply an opacity layer to the image.
@media (prefers-color-scheme: dark) {
img:not([src*=".svg"]) {
opacity: 80%;
}
}
This will make most of the images look pretty dull, so we can instead allow users to show the fully bright version of the image by hovering over the image.
We need to add the events :hover for desktop users and :active for mobile users.
@media (prefers-color-scheme: dark) {
img:not([src*=".svg"]) {
opacity: 80%;
transition-duration: 400ms;
transition-property: opacity;
}
img:not([src*=".svg"]):hover, img:not([src*=".svg"]):active {
opacity: 100%;
}
}
Now, the images will be darkened slightly if the colour scheme is dark, but the user can hover over them to see them in their full brightness.
If you can't change your images in CSS then an alternative approach is to use responsive images. Using the picture element we can set a number of different images that are then shown depending on what the current colour scheme is. We can supply a media attribute to each source element that will tell the browser which image to use for that slot.
The following example shows this in action, with the light image used as a default and the dark image used when the user has the dark colour scheme selected.
<picture>
<source srcset="image-dark.png" media="(prefers-color-scheme: dark)" />
<source srcset="image-light.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" />
<img src="image-light.png" />
</picture>
This is less useful for content images as you need to upload multiple versions of each image for every image you need on the site.
Functional Images
Functional images are things like icons and other graphics that show that an element has some form of functionality. This is things like a little icon next to external links to show that the user will leave the site if they click on them, or even an icon that shows the current colour scheme in use.
All these images will need to have an alternate version for the light and dark colour schemes. Assuming that these images are added through background-image properties, changing them becomes a task of altering the property value based on the current colour scheme.
.some-class {
background-image: url(../images/icon-link-light.svg);
}
@media (prefers-color-scheme: dark) {
.some-class {
background-image: url(../images/icon-link-dark.svg);
}
}
Using this mechanism you can then ensure that the icons on your site will still function correctly. This is pretty much the same mechanism that we used to change the colours on elements elsewhere on the site.
Now that we have set up our colour schemes, we need a way for the user to be able to ignore that and select their own scheme.
After some experimentation I found the most reliable way of allowing this was to inject a class into the body tag of the page and force the colour scheme to be either light or dark. The styles to do this are pretty straight forward, we just detect the class being used and set the colour scheme using the color-scheme property.
.light-theme {
color-scheme: light;
}
.dark-theme {
color-scheme: dark;
}
With this in place all we have to do is inject the class to change the colours on the page. The light-dark() function will still work with these alterations in place, but you still need to set the root colour scheme of "light dark" so that the correct colours can be selected.
To allow the user to select their preference we need to add a button, here is a typical button that can be used to facilitate this.
<div>
<button class="light-dark-toggle" aria-label="Switch color theme"><span></span></button>
</div>
The div element exists so that you can place the icon on your page within the other elements of the page without having to fiddle with the styles of the button component. Adding the styles for the button we also inject a background image to act as a light/dark indicator for the user.
.light-dark-toggle {
background: none;
border: none;
span::before {
content: "";
background-image: url(../images/icons/sun.svg);
width: 25px;
display: block;
height: 25px;
cursor: pointer;
}
}
I found some free and open source sun and moon SVG images for this purpose, they aren't difficult to find.
To change the background image of the button we need to add the following styles. This tells the browser that if the user prefers the dark colour scheme and the light mode is not enabled, then change the icon in the button to a moon. This is needed so that if the prefers-color-scheme media query tells us that the user wants to see dark mode, but they have selected light mode, then to honour that request.
@media (prefers-color-scheme: dark) {
body:not(.light-theme) {
.light-dark-toggle {
span::before {
background-image: url(../images/icons/moon.svg);
}
}
}
}
Let's create an action for the button. What we need to do here is to look for the current class applied to the page, falling back to the prefers-color-scheme property of the page if that isn't set. Once we have the information we need we then apply the correct class to the body tag of the page, which changes the colour scheme selected.
Once we have done this we also store that information in local storage. This is so that if the user moves to a different page we can pull that value out of the local storage and make sure that their selected colour scheme is applied on the other pages of the site as well.
const btn = document.querySelector(".light-dark-toggle");
btn.addEventListener("click", function lightDarkClickAction() {
const hasDarkClass = document.body.classList.contains("dark-theme");
const hasLightClass = document.body.classList.contains("light-theme");
if (hasDarkClass === false && hasLightClass === false) {
// User doesn't have a theme class set, so use their os settings to add it.
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
// User prefers dark, swap to the light theme.
document.body.classList.add("light-theme");
} else {
// User prefers light, swap to the dark theme.
document.body.classList.add("dark-theme");
}
} else if (hasDarkClass) {
// Dark mode class is set, swap to light.
document.body.classList.remove("dark-theme");
document.body.classList.add("light-theme");
} else if (hasLightClass) {
// Light mode class is set, swap to dark.
document.body.classList.add("dark-theme");
document.body.classList.remove("light-theme");
}
let theme = "light";
if (document.body.classList.contains("dark-theme")) {
theme = "dark";
}
localStorage.setItem("theme", theme);
});
When the page loads we need to ensure that we look for the user's preference in local storage and apply the body class depending on what the value is.
function colourSchemeInit() {
if (localStorage.getItem("theme") === "dark") {
// Override class with user's dark theme preference.
document.body.classList.add("dark-theme");
} else if (localStorage.getItem("theme") === "light") {
// Override class with user's light theme preference.
document.body.classList.add("light-theme");
}
}
colourSchemeInit();
In summary, when the user loads the page we look for any previous setting of the colour scheme. If it has been set then we apply the class to the body element to force this colour scheme to be set. If the user then toggles the colour scheme button again then we know what they previously selected and swap to the other version.
If you also want to allow the user to remove their preference you can add an additional button that gives users the ability to control this.
<div>
<button class="light-dark-clear" aria-label="Clear color theme">Clear colour preferences</button>
</div>
The click action of the button, which just clears the local storage for the theme setting.
const clearButton = document.querySelector(".light-dark-clear");
clearButton.addEventListener("click", function clearButtonAction() {
// Remove the set classes.
document.body.classList.remove("light-theme");
document.body.classList.remove("dark-theme");
// Remove the local storage.
localStorage.removeItem("theme");
});
This clears the local storage and resets the theme back to the operating system or user agent default setting that the user originally had.
Remember that this is a user preference and you should be careful to not change what the user wants without their direct involvement. Setting the colour scheme on a page without the user's understanding can lead to annoyance and frustration.
For extra flair you can add transitions for the colours for your theme so that when the user clicks on the theme button they don't see the screen immediately swap from one colour to the other. They instead see the colours fade from one colour scheme to the other.
We could add this transition by just applying the transition-duration style to every element on the site as it changes colours of the different coloured properties.
* {
transition-duration: 500ms;
transition-property: color, background-color, border-color,
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
backdrop-filter;
}
This method, however, has an issue. If the user refreshes the page or goes to a different page on your site then they might see the transition between one colour scheme to the other, depending on their preferences. There will be a small delay between the page loading and the classes that change the colour scheme so the user may see the transition animation when the classes are added.
The solution to this is to prevent the transitions from running until we are ready by putting them behind a class called theme-transition. If the class isn't present on the page then the transitions won't run.
.theme-transition * {
transition-duration: 500ms;
transition-property: color, background-color, border-color,
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
backdrop-filter;
}
All we need to do now is add the theme-transition class to our body element after the page has finished loading and all of the correct classes have been added. We could just wait for the page load event to trigger, but by adding a small delay (using the setTimeout() function) then we can ensure that the class is added after we have finished applying colour scheme to the page.
setTimeout(() => {
document.body.classList.add("theme-transition");
}, 1000);
With this in place we don't see an animation when the page loads, but when the user clicks the theme toggle button the transition animations are run correctly.
We should also add a media query to remove the transitions if the user has that option enabled in their user agent settings, or if the user is on a device with a slow update time (like an e-ink display).
.theme-transition * {
transition-duration: 500ms;
transition-property: color, background-color, border-color,
text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter,
backdrop-filter;
@media screen and (prefers-reduced-motion: reduce), (update: slow) {
transition-duration: 0s;
}
}
This prevents the transition of the colour scheme from annoying the user. We could also allow the user to remove all colour transition animations from our site by removing the theme-transition class from the body tag.
One slight word of warning with this method is that if you have any existing transitions for colours then you will need to make sure that they still work with this system in place. This rule might get more specificity than your local animations and override them, but that depends on how you apply your transition rules.
With all of that in mind, here are some tips on creating and using colours schemes.
Plan ahead! If all of your colours aren't already collected into variables in a single place then you should do that first. Then you can start thinking about adding in dark mode colour schemes.
Decide on your "default" colour scheme and then add CSS variables for each of your schemes based on the default version of your page. For example, if your default is light then set those values up and then fall back to dark colours using media queries or the "light dark" system.
Remember that your page probably has a number of colours that you wouldn't immediately think about. Everything from drop shadows, border colours, background colours, and event link hover states needs to be thought about.
A good tip when starting out is to set up your colour scheme and toggle selection button and then set all of your "dark" colours to black. Then, when you swap to that colour scheme your site should be totally black. If it isn't then you have missed a colour out and need to update your style sheets to ensure that the element is captured by your colour scheme rules. Once you have the page one colour you can add in your dark brand colours and watch the site come to life.
If you are unsure if you have missed anything then a quick ctrl+a (i.e. select all) will show you any text elements you have missed.
Have a think about the images on your site. Background images can be easily swapped out for different schemes, but images in content can be a problem. Using responsive images and different images for schemes is a good way to get around some of the static images on your site but can be a pain for content as you'll need multiple versions.
Try to avoid images with coloured backgrounds that can't adapt to your colour scheme. Swapping to the dark theme and seeing a big white square with an image in the middle will look messy.
Always give your users the ability to override the default colour scheme. Once this is set, do your upmost to honour that preference for every page on your site.
If you have visual regression testing then you need to have the ability to test for the light and dark versions of your theme. You can set this using a flag, but it's often simpler to run a regression test on a page, click the colour scheme change, and then run the regression tests in the dark mode of the same page.
Be careful about accessibility of your colours as well. If you have tested the colour difference on your light colour scheme but have neglected your dark scheme then you might only have a half accessible site.
Allowing your users to select a colour scheme they prefer will make them feel more inclined to stay on your site and read your content. Plus, they won't need to use browser plugins or sunglasses to fix the colours themselves.
It does take a bit of planning as you can't just tweak some colours and consider it done. You need to ensure that all colours for each colour scheme is accounted for.
When this site was first created it had a theme with white text on a black background. I would get complaints about it almost every week as people would complain that they couldn't read it. That was the original reason that I changed to black text on a white background, but I also got the occasional complaint about the site being too bright.
Giving readers on this site the ability to select their own colour scheme is a real benefit, and I have already made the dark mode my default colour scheme.
If you want to play around with the concepts involved in this article then I have created a codepen that shows the light and dark mode CSS in action, along with a theme switcher.
color-scheme from the Mozilla developer documentation pages.
color-scheme meta tag from the Mozilla developer documentation pages.
light-dark() from the Mozilla developer documentation pages.
Add new comment