Unlocking Dark Mode With CSS3

# darkmode# learn2code

     Dark mode is becoming more popular for not just apps, but websites as well. So much so that it can be found on some of the most popular websites across the web. Adding a dark mode is not a feature exclusive to large teams and does not require rebuilding your sites from scratch. With a bit of planning, you can add dark mode to any site or PWA. 

Download the project files here

Part 1: The Setup

     Let's create a baseline page we can work with up front for this demo. Make a folder and create the following files:

  •  index.hml
  •  style.css
  •  code.js

With those created we have a good foot forward for success. Let's set the content for our demo website. Copy the code below. I will separate the file name from the content with three dashes for simplicity.

Index.html

<html>
    <head>
        <link rel="stylesheet" href="style.css"/>
    </head>
    <body>
        <h1>I'm a Webpage</h1>
        <p>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
            tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
            veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
            ea commodo consequat.
        </p>
        <script type="text/javascript" src="code.js"></script>
    </body>
</html>

 

     This is a fairly basic page and we will cover more complexity, but the prinicples we'll cover work across the board once we lay the groundwork here.

Style.css

html[data-theme='light'] {
    color: #000;
    background: #fff;
}

html[data-theme='dark'] {
    color: #fff;
    background: #000;
}

 

     The style here uses css selectors based on the data tag 'theme'. in HTML this looks like <html data-theme='light'>. Please note the lack of space between html and the opening brace [ . That is important for targeting and won't work reliably otherwise. If you're having issues getting the theme to switch, check here first. We could instead use CSS classes if we want, but data attribute changes provide a targeted approach we can apply across the board.

Code.js

function loadLightMode() {
    document.documentElement.dataset.theme = 'light';
}

function loadDarkMode() {
    document.documentElement.dataset.theme = 'dark';
}

 

     Two functions are used for swapping between light and dark theme here by simply applying the data tag for the theme we want. The dataset property is one of the great new features in HTML5 that simplifies working with data-* attributes on page elements. If you have questions Mozilla has a great breakdown to read more about it here. You can trigger these functions manually at the moment by calling them from the developer tools debugger console in your browser.

Part 2: The Complexity Problem

     The next step is to make a more concrete example based on what we've done so far. First we're going to modify the index.html file to add more diverse content that better represents what we may have on an average page.

Index.html

<html>
    <head>
        <link rel="stylesheet" href="style.css"/>
    </head>
    <body>
        <nav>
            <label for="themeSelect">Theme </label><select id="themeSelect">
                    <option value="light">Light</option>
                    <option value="dark">Dark</option>
                </select>
            <ul class="navbar">
                <li><a href="#">Home</a></li>
                <li><a href="#">Tree Views</a></li>
            </ul>
        </nav>    
        <h1 class="title">The Daily Arborist</h1>
        <div class="row">
            <article>
                <h2 class="title">Tree Nightmares</h2>
                <p>What you REALLY need to know about living on Elm street.</p>
            </article>
        </div>
        <div class="row">
            <article>
                <h2 class="title">Finding High Ground</h2>
                <p>Water everywhere but not a drop to drink. Making sure your trees
                    can survive under water in the coming distopian floods.
                </p>
            </article>
        </div>
        <script type="text/javascript" src="code.js"></script>
    </body>
</html>
 

     Once you've made the change, refresh the index page in your browser and see the difference. Make sure it loads properly and doesn't have any weird tags hanging out, then we need to update the CSS so the page has the basic styling we want from our new layout. 

Style.css

/**
Theme Setup
**/
html[data-theme='light'] {
    color: #000;
    background: #fff;
}

html[data-theme='dark'] {
    color: #fff;
    background: #000;
}

/**
Basic page layout
**/
body { margin: 0; }
h1,h2,h3,h4,h5 { color: #225; }
nav { background: #ccc; }
.navbar {
    text-align: center;
    list-style: none;
    font-size: 1.7em;
    height: 80px;
}
.navbar > li { display: inline; }
.title { text-align: center; }
article
    background: #ccc;
    margin-bottom: 10px;
}
 

     These CSS changes set up the basics of a page with a main background color, content background colors, and varied header colors.

     Add the following lines to the top of the code.js file and don't delete the other lines. Just add these above the previous ones.

Code.js

window.addEventListener("load", themeBootstrap);

function themeBootstrap() {
    document.getElementById("themeSelect").addEventListener("change", updateTheme);
}

function updateTheme() {
    var selectedOption = this.querySelector("option:checked");
    switch(selectedOption.value.toLowerCase()) {
        case 'dark':
            loadDarkMode();
            break;
        case 'light':
        default:
            loadLightMode();
    }
}

 

     With these lines added we're doing quite a bit more, so let's step through this code for a moment. 

Line 1 window.addEventListener(...) : First we're adding an event listener to the window's load event. This will call our themeBootstrap function once the window is finished loading, but before all the content itself for the document completes displaying (i.e. images may not be done loading before this is called).

Line 3 function themeBootstrap() : The bootstrapper is a stripped down function where we can call all of our event listeneres that we want to be related to the theme. In this case, we add another listener that monitors the select box we've added to the top left corner of the page so when the selection is changed it will automatically call the updateTheme function.

Line 7 function updateTheme(): Here we're finding the selected option for the select box and passing the value property into a switch, which then calls the appropriate function based on the user's selection. It's important to note we're calling toLowerCase() on the value so the values match the cases, since 'Light' and 'light' are not the same string values. 

     At this point when you load the page up you should be able to change the theme using the select box in the top left corner. The result however is horrible and unusable, since colors don't work, at all. In the following steps we're going to fix the CSS file to centralize our colors using CSS Variables or Custom Properties (they're the same thing). For more a more in-depth look at them check out the Mozilla foundation's info here.

     First, let's move the color values we want for our primary theme to the top light theme, and also update the main light theme also target the html page in the event that we don't have a theme selected by just adding another selector. 


html, html[data-theme='light'] {
    --color-text-primary: #000;
    --color-text-header: #225;
    --color-background-primary: #fff;
    --color-background-secondary: #ccc;
    color: #000;
    background: #fff;
}
 

 

next we update the color and background values throughout the page to use these variables by wrapping the variable name in var()


/**
Theme Setup
**/
html, html[data-theme='light'] {
    --color-text-primary: #000;
    --color-text-header: #225;
    --color-background-primary: #fff;
    --color-background-secondary: #ccc;
    color: var(--color-text-primary);
    background: var(--color-background-primary);
}
 
/**
Basic page layout
**/
body { margin: 0; }
h1,h2,h3,h4,h5 { color: var(--color-text-header); }
nav { background: var(--color-background-secondary); }
.navbar {
    text-align: center;
    list-style: none;
    font-size: 1.7em;
    height: 80px;
}
.navbar > li { display: inline; }
.title { text-align: center; }
article
    background: var(--color-background-secondary);
    margin-bottom: 10px;
}

 

     The above changes the values so they're bound to our new variables, and allows us to adjust them in dark mode. Now we can override these variables when we change to dark mode in one place, without having to update everything and chase a ton of values in a bunch of different areas. 


html[data-theme='dark'] {
    --color-text-primary: #fff;
    --color-text-header: #aaf;
    --color-background-primary: #000;
    --color-background-secondary: #444;
}
 

     With the above change we're modifying the values to make everything more viewable and attractive to our readers while also shifting the background colors to a darker shade. There is another benefit to moving the colors up to these two themes and using variables everywhere else: we can add new color themes with very little effort in the way of code. You just add another theme layout in your theme section, then add the appropriate dropdown menu option to match, and add the appropriate fix to the JS.


Part 3: Remember Me?

     The code above is a good solution, but doesn't do one very important thing: it doesn't remember what your users selected when the page reloads. We can add that functionality thanks to another new feature called Local Storage. You can find more detailed info about local storage from the Mozilla Developer Network here.
 
We're going to add the following code to the code.js file to implement saving the theme when it's changed, as well as reading the theme on first load. First up let's add code to save the conent.

Code.js
function updateTheme() {
    var selectedOption = this.querySelector("option:checked");
    var value = selectedOption.value.toLowerCase();
    switch(value) {
        case 'dark':
            loadDarkMode();
            break;
        case 'light':
        default:
            loadLightMode();
    }
    saveTheme(value);
}

function saveTheme(themeName) {
    localStorage.setItem("theme",themeName);
}
 
     The primary change here is we've added the bottom line to updateTheme() that calls our new save function, and then we've got the new saveTheme() function that calls localStorage and saves the theme name to a key named "theme". This key will persist through multiple loads, and the best part is nobody has to make an account to use it. 
 
Code.js
 
function themeBootstrap() {
    loadTheme();
    document.getElementById("themeSelect").addEventListener("change", updateTheme);
}
 
function loadTheme() {
    var theme = localStorage.getItem('theme') || 'light';
    var selector = document.querySelector("#themeSelect");
    var index = [].slice.call(selector.options).find(x => x.value === theme).index;
    selector.selectedIndex = index;
    updateTheme.call(selector);
}

 

     The first update adds a call to loadTheme() so we can call the selected theme once the page loads. The second update adds loadTheme(), which first gets the value from local storage (or applies 'light' as the default should there not be a saved theme). Then it grabs the select box and finds the matching option to the theme, sets the index for the select box to that option (so the select box always shows the properly selected value on initial load), and then calls updateTheme() to manage the actual theme load.