Dark Mode
“Dark mode” color schemes feature light foreground colors on dark background colors, resulting in less light emitted from the display than with “light mode” color schemes. Dark mode is great in low-light environments and uses less energy when using OLED displays. Some people find that dark mode reduces eye strain due to the low light, while others find that dark mode increases eye strain due to poor contrast. Some people adjust the color scheme according to the time of day, using light mode during the day and dark mode before going to sleep.
Personally, I prefer different color schemes in different applications. I spend most of my time in terminals, where I use dark mode. I do not use dark mode in my browser, however, as I prefer to see web content as designed.
Some operating systems allow users to configure a preference between dark mode and light mode, and this preference is exposed to web content via the CSS prefers-color-scheme media query. Using this feature, developers can automatically use a dark mode color scheme or light mode color scheme according to the user’s preference. As I write this, browser support is pretty good.
I plan on adding support for dark mode to this website in the (near?) future. I am currently working on FeedPipe, and I realized that adding support for dark mode to the built HTML manuals would be good practice. I am not ready to release FeedPipe yet, so I created a small demo for this blog post.
The objectives for the manuals and demo are as follows:
- The implementation must be a single HTML file. All images, CSS, and JavaScript must be embedded in the HTML, not separate files.
- The implementation must switch between dark mode and light mode according to the media query. If the user has not manually set a mode, the page must switch modes automatically when there is a change of preference. This must work even when JavaScript is disabled.
- The implementation must use light mode by default. In other words, light mode is used if the media query is not supported.
- When JavaScript is enabled, a control icon must be displayed to allow the user to switch modes manually. Clicking on the icon switches modes and disables automatic switching based on preference. Different icons must be displayed in dark mode and light mode, and no icons are displayed when JavaScript is disabled.
- When JavaScript is enabled, the favicon must be updated to indicate the current mode. When JavaScript is disabled, no favicon is displayed.
The demo is available on GitHub: dark-mode-demo.html
.
Please note that the code for this demo is only demonstration quality
and has not been polished or extensively tested.
Overview
The HTML file has the following structure:
<!doctype html>
html lang="en">
<head>
<meta charset="utf-8" />
<title>Dark Mode Demo</title>
<link rel="icon" id="favicon" href="javascript:void(0)" />
<style>
<
...style>
</head>
</body class="scheme-auto">
<main> ... </main>
<div id="scheme-switch"> ... </div>
<div class="hidden"> ... </div>
<script> ... </script>
<body>
</html> </
The favicon
identifier has to be defined so that it can
be referenced by JavaScript.
The style
part contains the CSS definitions, and the
body
has four parts:
- The
main
part defines the content. - The
div
with identifierscheme-switch
part defines the control interface. - The
div
with classhidden
part contains the icons. - The
script
part contains the JavaScript code.
The body
element is assigned class
scheme-auto
by default. This class indicates that the user
has not manually switched modes. When a user manually switches modes,
this class is removed. Class scheme-dark
is used to
indicate that dark mode is selected. No class is used to indicates that
light mode is selected, since light mode is the default.
Style
In this minimal demo, only the background-color
and
color
of the body
element need to change
according to the color scheme. The default is light mode.
body {background-color: #fff;
color: #121212;
}
If dark mode is selected manually, then the body
element
is assigned class scheme-dark
.
.scheme-dark {
bodybackground-color: #121212;
color: #eee;
}
The demo content is just a message in the center of the screen. The following CSS incantations center the text and select the font.
, html {
bodyheight: 100%;
margin: 0;
}
main {align-items: center;
display: flex;
height: 100%;
justify-content: center;
}#msg {
font-family: "Noto Sans", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 48pt;
font-weight: bold;
}
The control interface is displayed at the top right of the screen. Note that it has the same background color in both light mode and dark mode.
.control {
background-color: #121212;
border-radius: 4px;
cursor: pointer;
padding: 4px;
position: absolute;
right: 4px;
top: 4px;
width: 26px;
}
As is customary, the hidden
class is used to hide things
from the screen.
.hidden {
display: none;
}
The following media query is used define styles when on a screen and
the user prefers dark mode. The styles should all start with
body.scheme-auto
so that they are only used when the user
has not manually selected a mode.
@media screen and (prefers-color-scheme: dark) {
.scheme-auto {
bodybackground-color: #121212;
color: #eee;
} }
The following media query is used to define styles when printing.
Note that light mode is used when printing, but transparent
background colors are used instead of white
(#fff
). The styles are marked as !important
to
make sure that they take precedence.
@media print {
body {background-color: transparent !important;
color: black !important;
}.control {
display: none !important;
} }
In summary, colors must be set in four places:
- The default styles (
body
) use light mode colors. - The
scheme-dark
styles (body.scheme-dark
) use dark mode colors. These styles are used when dark mode is selected manually, not when dark mode is selected by the media query. - Under the
(prefers-color-scheme: dark)
media query, thescheme-auto
styles (body.scheme-auto
) use dark mode colors. These styles are used when dark mode is selected by the media query, not when dark mode is selected manually. - Under the
print
media query, the styles (body
) use light mode colors, withtransparent
background colors instead ofwhite
(#fff
).
Content
The demo content is simply a message that is displayed in the center of the screen.
main>
<div id="msg">Dark Mode Demo</div>
<main> </
Control Interface
The control interface contains two icons, one for light mode and one for dark mode. The control interface is hidden by default, as it is only shown when JavaScript is enabled. JavaScript shows one icon and hides the other, according to the current mode.
div id="scheme-switch" class="control hidden">
<span id="scheme-switch-dark"> ... </span>
<span id="scheme-switch-light"> ... </span>
<div> </
I first implemented the demo using Firefox and used emoji for the icons.
span id="scheme-switch-dark">🌙</span>
<span id="scheme-switch-light">💡</span> <
Unfortunately, Chrome does not support emoji yet. (Chrome is the IE 6 of Today!) To make the demo work in Chrome, I needed to embed the icons into the HTML. I was able to get the SVG source for the Firefox icons:
The SVG is embedded in a separate part of the page and referenced by identifier in the control interface code. For example, the following is a reference to the moon icon.
svg width="24" height="24" x="0px" y="0px" viewBox="0 0 512 512">
<use xlink:href="#moon"></use>
<svg> </
The icons are defined in the hidden div
as follows:
div class="hidden">
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<g id="moon">
<
...g>
</g id="bulb">
<
...g>
</defs>
</svg>
</div> </
JavaScript
The JavaScript code is organized as follows:
function() {
("use strict";
function makeDataURL(foreground, background) { ... }
const faviconEle = document.getElementById("favicon");
const lightDataURL = makeDataURL("#121212", "#fff");
const darkDataURL = makeDataURL("#eee", "#121212");
var isDarkScheme = false;
var isManual = false;
var query = null;
function updateIcons() { ... }
function setSchemeFromQuery() { ... }
function setSchemeManual() { ... }
function init() { ... }
init();
; })()
The makeDataURL
function creates a favicon that indicates
the current color scheme.
function makeDataURL(foreground, background) {
const canvas = document.createElement("canvas");
.height = 32;
canvas.width = 32;
canvasconst ctx = canvas.getContext("2d");
.fillStyle = background;
ctx.fillRect(0, 0, 32, 32);
ctx.fillStyle = foreground;
ctx.fillRect(10, 10, 12, 12);
ctxreturn canvas.toDataURL("image/png");
}
The favicon
element is loaded, and the icons for both
modes are calculated once.
The isDarkScheme
variable is set to true
when in dark mode. The isManual
variable is set to
True
when the user selects a mode manually. The
query
variable is used for interfacing with the media
query, if matchMedia
is supported by the
browser.
The init
function is run when the page loads. If
matchMedia
is supported by the browser, then the media
query is initialized, an event listener is added so that any changes of
preference are handled, and the mode is set according the current
preference. In any case, a click event handler is added to the control
interface, and the control interface is displayed.
function init() {
if (window.matchMedia) {
= window.matchMedia("(prefers-color-scheme: dark)");
query .addEventListener("change", setSchemeFromQuery);
querysetSchemeFromQuery();
}const switchEle = document.getElementById("scheme-switch");
.onclick = setSchemeManual;
switchEle.classList.remove("hidden");
switchEle }
When the user changes preference, the setSchemeFromQuery
function is called. If the user has not selected the mode manually, the
current preference is updated and the icons are updated.
function setSchemeFromQuery() {
if (!isManual) {
= query.matches;
isDarkScheme updateIcons();
} }
The updateIcons
function updates the control interface
icon and favicon
according to the current mode.
function updateIcons() {
if (isDarkScheme) {
document.getElementById("scheme-switch-light").classList.remove("hidden");
document.getElementById("scheme-switch-dark").classList.add("hidden");
.href = darkDataURL;
faviconEleelse {
} document.getElementById("scheme-switch-dark").classList.remove("hidden");
document.getElementById("scheme-switch-light").classList.add("hidden");
.href = lightDataURL;
faviconEle
} }
When the user clicks on the control interface, the
setSchemeManual
function is called. This function sets the
isManual
flag, toggles the isDarkScheme
flag,
updates the icons, and updates the body
class that
indicates the current mode.
function setSchemeManual() {
= true;
isManual = !isDarkScheme;
isDarkScheme updateIcons();
if (isDarkScheme) {
document.body.classList.add("scheme-dark");
else {
} document.body.classList.remove("scheme-dark");
}document.body.classList.remove("scheme-auto");
}
Testing
To test, download the demo and open it in your browser. I recommend using Firefox, which has built-in developer tools that make it easy to do the following tests.
- Load the demo with JavaScript enabled.
- The mode should be set according to your preference.
- The control interface should be displayed.
- The favicon should indicate the mode.
- Use your browser developer tools to toggle your preference.
- The mode should change automatically.
- The icon in the control interface should change according to the mode.
- The favicon should change according to the mode.
- Click on the control interface.
- With each click, the mode should change.
- The icon in the control interface should change according to the mode.
- The favicon should change according to the mode.
- Use the browser developer tools to toggle your preference.
- The mode should not longer change because you have selected a mode manually.
- When in light mode, display a print preview.
- The print preview should be in light mode.
- The control interface should not be printed.
- When in dark mode, display a print preview.
- The print preview should be in light mode.
- The control interface should not be printed.
- Use the browser developer tools to disable JavaScript for the demo
tab.
- The mode should be set according to your preference.
- The control interface should not be displayed.
- There should be no favicon for the tab.
- Use your browser developer tools to toggle your preference.
- The mode should change automatically even with JavaScript disabled.