Implementing Dark Theme for the Web

A summary of various approaches to style a webpage to match the user’s system theme using CSS, HTML and native javascript.

Approach 1: Just CSS — `prefers-color-scheme’

The classic method: detect user’s theme preference with CSS media feature prefers-color-scheme .

While a completely different set of styles can be specified for each theme; using CSS variables allows for less repetition.

Link to full code

/* file: style.css *//* default: light theme colours */
:root {
--bg-colour: #fafafa; /* white */
--font-colour: #212529; /* dark grey*/
--primary-colour: #2196f3; /* blue */
--link-colour: #2962ff; /* blue */
--alt-bg-colour: #fff; /* white */
}

/* if system theme is dark */
@media(prefers-color-scheme:dark) {
:root {
--bg-colour: #000; /* black */
--font-colour: #fff; /* white */
--primary-colour: #90caf9; /* light blue */
--link-colour: #81d4fa; /* light blue */
--alt-bg-colour: #5a5a5a; /* grey */
}
}
/* main webpage styling */
body {
background-colour: var(--bg-colour);
color: var(--font-color);
}
/* ... rest of styling */

Approach 2: HTML & CSS — Embedding multiple stylesheets

The media attribute allows us to specify the media type (in this case, light/dark system theme) the target resource (stylesheet file) is for.

Link to full code

<!-- file: index.html --><head>
<!-- other elements in head-->
<!-- stylesheet with dark theme css variables -->
<link rel="stylesheet" media="(prefers-color-scheme:dark)" href="dark.css">

<!-- stylesheet with light theme css variables -->
<link rel="stylesheet" media="(prefers-color-scheme:light)" href="light.css">
<!-- base stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<!-- rest of html -->/* file: light.css */:root {
--bg-colour: #fafafa; /* white */
--font-colour: #212529; /* dark grey*/
--primary-colour: #2196f3; /* blue */
--link-colour: #2962ff; /* blue */
--alt-bg-colour: #fff; /* white */
}
/* file: dark.css */:root {
--bg-colour: #000; /* black */
--font-colour: #fff; /* white */
--primary-colour: #90caf9; /* light blue */
--link-colour: #81d4fa; /* light blue */
--alt-bg-colour: #5a5a5a; /* grey */
}
/* file: style.css */body {
background-colour: var(--bg-colour);
color: var(--font-color);
}
/* ... rest of styling */

The media attribute decides which stylesheet to populate the CSS variables to be used in style.css to style the webpage. For a more detailed look into how this method works and the loading strategy, Thomas Steiner has a well written article with more details on this method.

Approach 3: JS — Changing the style/stylesheet

3.1 Changing CSS variables

This approach changes the CSS variables to correspond to the user’s system theme using native javascript.

Link to full code

<!-- file: index.html -->
<head>
<!-- other elements in head-->

<!-- base stylesheet -->
<link rel="stylesheet" href="style.css">
<!-- script -->
<script src="script.js"></script>
</head>
<!-- rest of html -->/* file: style.css *//* set light theme colours as default */
:root {
--bg-colour: #fafafa;
--font-colour: #212529;
--primary-colour: #2196f3;
--link-colour: #2962ff;
--alt-contrast-colour: #fff;
}
/* ... rest of styling */

To detect whether the user’s system using dark theme in javascript, a method is to use window.matchMedia("(prefers-color-scheme:dark)").matches, which checks whether the document (webpage) matches the media query string ( prefers-color-scheme:dark — whether the user’s system theme is dark).

// file: script.js// light theme colours
const lightTheme = {
"--bg-colour": "#fafafa",
"--font-colour": "#212529",
"--primary-colour": "#2196f3",
"--link-colour": "#2962ff",
"--alt-contrast-colour": "#fff"
};
// dark theme colours
const darkTheme = {
"--bg-colour": "#000",
"--font-colour": "#fff",
"--primary-colour": "#90caf9",
"--link-colour": "#81d4fa",
"--alt-contrast-colour": "#5a5a5a"
};
// root element to be used later to change CSS variables' values
let root = document.documentElement;
/**
* Changes the webpage's colours based on colours object passed in
*
* @param {object} themeColours object with the CSS variables and their corresponding colour values
*/
function setTheme(themeColours) {
console.log("setting theme: " + themeColours);
for (var name in themeColours) {
root.style.setProperty(name, themeColours[name]);
};
};
// waits for DOM to finish loading
document.addEventListener("DOMContentLoaded", function(event) {
// initial determination of user's system theme
var isDarkTheme = (window.matchMedia("(prefers-color-scheme: dark)"));
(isDarkTheme.matches)?setTheme(darkTheme):setTheme(lightTheme);
// when user changes system theme, also change webpage colours
isDarkTheme.addEventListener('change', (event) => {
(isDarkTheme.matches)?setTheme(darkTheme):setTheme(lightTheme);
});
});

3.2 Changing the stylesheet path

The stylesheets for this approach (light.css, dark.css, style.css) are the same as already mentioned for approach 2.

Link to full code

<!-- file: index.html -->
<head>
<!-- other elements in head-->

<!-- default: light theme css variables -->
<link rel="stylesheet" href="light.css" id="theme-colours">
<!-- base stylesheet -->
<link rel="stylesheet" href="style.css">
<!-- script -->
<script src="script.js"></script>
</head>
<!-- rest of html -->// file: script.js// theme colours
const lightThemeStylesheet = "light.css";
const darkThemeStylesheet = "dark.css";
// gets element for the stylesheet that sets theme colours
var themeLink = document.getElementById("theme-colours");
// waits for DOM to finish loading
document.addEventListener("DOMContentLoaded", function(event) {
// initial determination and setting of user's system theme
var isDarkTheme = (window.matchMedia("(prefers-color-scheme: dark)"));
// sets themeLink
themeLink.href = isDarkTheme.matches ? darkThemeStylesheet : lightThemeStylesheet;
// when user changes system theme, also change webpage's stylesheet
isDarkTheme.addEventListener('change', (event) => {
themeLink.href = isDarkTheme.matches ? darkThemeStylesheet : lightThemeStylesheet;
});
});

3.3 Changing data attribute `data-theme`

This approach utilises data attributes which allows us to store additional information to the webpage’s HTML. In this case, javascript detects and stores the user’s system theme in data-theme.

Link to full code

<!-- file: index.html --><html lang="en">
<head>
<!-- other elements in head-->

<!-- base stylesheet -->
<link rel="stylesheet" href="style.css">
<!-- script -->
<script src="script.js"></script>
</head>
<!-- rest of html -->
</html>

In index.html, <html> does not have the data-theme attribute, this is not a mistake — the default for the root CSS variables is set to light theme in style.css (so there is a default style to fall back to) and the attribute is set in script.js.

/* style.css *//* default: light theme colours */
:root {
--bg-colour: #fafafa;
--font-colour: #212529;
--primary-colour: #2196f3;
--link-colour: #2962ff;
--alt-contrast-colour: #fff;
}
/* dark theme colours */
html[data-theme="dark"] {
--bg-colour: #000;
--font-colour: #fff;
--primary-colour: #90caf9;
--link-colour: #81d4fa;
--alt-contrast-colour: #5a5a5a;
}
/* ... rest of styling */// style.js// get html element
const htmlEl = document.querySelector("html");
// waits for DOM to finish loading
document.addEventListener("DOMContentLoaded", function(event) {
// initial determination and setting of user's system theme (data-theme)
var isDarkTheme = (window.matchMedia("(prefers-color-scheme: dark)"));
htmlEl.dataset.theme = isDarkTheme.matches ? "dark" : "light";

// when user changes system theme, also change webpage's stylesheet
isDarkTheme.addEventListener('change', (event) => {
htmlEl.dataset.theme = isDarkTheme.matches ? "dark" : "light";
});
});

Extra: Allowing Users to Override the system’s default theme

Giving users choices! It is also a good idea to save the user’s chosen preferred theme choice (with localstorage) so that they next time they return, they would not need to set their preferred theme again.

For this, I added a theme switch toggle (original code by aliceyt) at the top right of the webpage. The toggle is automatically set the user’s system theme unless the user has previously set a preferred theme.

Some steps were also added to change up the navigation bar colour by changing its bootstrap class.

Link to full code: modified from approach 3.2 (same HTML) with additional CSS for the theme switch toggle.

// file: script.js// waits for DOM to finish loading
document.addEventListener("DOMContentLoaded", function(event) {
// theme colours
const lightThemeStylesheet = "light.css";
const darkThemeStylesheet = "dark.css";

// gets element for the stylesheet that sets theme colours
var themeLink = document.getElementById("theme-colours");
// element for theme switch toggle
var themeSwitch = document.getElementById("theme-switch");
// element for navbar
var navbar = document.getElementById("navbar");
/**
* Changes the webpage's colours, theme switch and navbar colour
*
* @param {string} "light" or "dark"
*/
function setTheme(theme) {
if (theme=="dark") {
localStorage.setItem("theme", "dark");
themeLink.href = darkThemeStylesheet;
themeSwitch.checked = true;
// removes bootstrap light navbar classes and add the dark navbar classes
navbar.classList.remove("bg-light", "navbar-light");
navbar.classList.add("bg-dark", "navbar-dark");
} else {
localStorage.setItem("theme", "light");
themeLink.href = lightThemeStylesheet;
themeSwitch.checked = false;
// removes bootstrap dark navbar classes and add the light navbar classes
navbar.classList.remove("bg-dark", "navbar-dark");
navbar.classList.add("bg-light", "navbar-light");
}
};
// get element of the theme switch toggle
var themeSwitch = document.getElementById("theme-switch");
var selectedTheme = localStorage.getItem("theme");
if (!selectedTheme) {
// if no theme in local storage, set to system default
(window.matchMedia("(prefers-color-scheme: dark)")).matches ? setTheme("dark") : setTheme("light");
} else {
setTheme(selectedTheme);
}
// when user toggles theme switch, change the theme
themeSwitch.addEventListener('change', (event) => {
(themeSwitch.checked) ? setTheme("dark") : setTheme("light");
});
});

This approach can be extended to utilise a dropdown/list of options and allow users more choices of themes (high-contrast etc.) — e.g. adding a stylesheet high-contrast.css and setting that as the stylesheet as selected.

Further Considerations

  • Ensure that there is sufficient contrast in colours such that text and interact-able components such as buttons stand out from the background and remain visible
  • It would be a good idea to also match the images to the theme. E.g. using darker images when the theme is dark, using a dark background on images of graphs
  • For branding, consider having logos that either work on both themes or have variations that can be adjusted for different themes

Student of tech, working in tech.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store