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.
/* 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.
<!-- 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.
<!-- 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.
<!-- 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
.
<!-- 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