Adding Dark Mode with HTML and CSS
2022-07-18 10:00:00 -0500

“Dark mode” was first introduced in 2016 on Windows, and macOS soon followed in 2019. But even though that was years ago, not all sites have added an automatic dark mode option. Until recently, my site was only available in light mode (AKA a light background with dark text, perfect for when it’s a bright day outside). I had previously held off on attempting dark mode with the impression it would be an HTML and CSS challenge. But I was pleasantly surprised!

Some websites, such as Twitter and Facebook, offer sticky settings where you can set the browser or app to always show that website in light or dark mode. Other websites give a little toggle button to switch between light and dark mode at will. And lastly, other websites allow the website to automatically change mode with your device settings. For example, I set my phone and computer to use light mode from 8am until 9:30pm, and if it’s not between those hours (AKA at night), then give me dark mode whenever possible. Implementing functions like sticky settings, toggle buttons, setting other user preferences, etc are more complex features that may include using browser cookies or a database. However, using CSS to automatically switch between light and dark mode with a device setting is much simpler, so that’s what we’ll walk through today!

NOTE: For the code examples below, I’ve attached all of the HTML/CSS required to give dark mode a try yourself. To use the examples, navigate to www.w3schools.com/css/tryit.asp, delete whatever HTML contents is currently there, copy-paste my example code. and press Run.

 

1. Make sure all existing colors on your site are in CSS, not HTML

The first step is to make sure that all of your color variables are set in CSS, versus set in HTML. Here’s a tiny HTML/CSS example where we set the color of a button directly in the HTML (see the third to last line):

View Code
<!DOCTYPE html>
<html>
  <head>
    <style>
      .button {
        background-color: green; /* Default color */
        border: none;
        color: white;
        cursor: pointer;
        display: inline-block;
        font-size: 16px;
        margin: 4px 2px;
        padding: 15px 32px;
        text-align: center;
        text-decoration: none;
      }
    </style>
  </head>

  <body>
    <h2>Button Colors Example</h2>

    <p>Check out my two buttons below!</p>

    <button class="button">Here's my button!</button>
    <button class="button" style="background-color: black">Here's my override button!</button>
  </body>
</html>

The part we need to focus on is style="background-color: black". That is where we set the color of the button to be black instead of the green default in the CSS. If your code has any instances of this type of thing (what may be typically indicated by a style part of the HTML), then it should change to something like this:

View Code
<!DOCTYPE html>
<html>
  <head>
    <style>
      .button {
        background-color: green; /* Default color */
        border: none;
        color: white;
        cursor: pointer;
        display: inline-block;
        font-size: 16px;
        margin: 4px 2px;
        padding: 15px 32px;
        text-align: center;
        text-decoration: none;
      }

      .override-background {
        background-color: black; /* This will override the green if the `override-background` class is on the HTML element */
      }
    </style>
  </head>

  <body>
    <h2>Button Colors Example</h2>

    <p>Check out my two buttons below!</p>

    <button class="button">Here's my button!</button>
    <button class="button override-background">Here's my override button!</button>
  </body>
</html>

In this change, we set an additional class called override-background on the second button, and then we can define the class’s attributes in the CSS section.

It may take some time to pull all of these styles out into CSS, but this will actually make your HTML and CSS code more well-formatted and more resilient, so this is a good refactor to be making anyway!

 

2. Pull out all existing colors in CSS into variables

Now that all of your colors are defined in CSS, you can begin pulling them out into variables. Here’s what this looks like:

View Code
<!DOCTYPE html>
<html>
  <head>
    <style>
      html {
        --button-text: white;
        --default-button-blackground: green;
        --override-button-background: black;
      }

      .button {
        background-color: var(--default-button-blackground);
        border: none;
        color: var(--button-text);
        cursor: pointer;
        display: inline-block;
        font-size: 16px;
        margin: 4px 2px;
        padding: 15px 32px;
        text-align: center;
        text-decoration: none;
      }

      .override-background {
        background-color: var(--override-button-background);
      }
    </style>
  </head>

  <body>
    <h2>Button Colors Example</h2>

    <p>Check out my two buttons below!</p>

    <button class="button">Here's my button!</button>
    <button class="button override-background">Here's my override button!</button>
  </body>
</html>

If you’ve completed Step 1 properly, there shouldn’t be changes needed on your HTML code.

It turned out my website had a lot of colors, and some of them I wasn’t even aware I was using (AKA I was using Bootstrap’s default colors). Don’t be afraid to continue adding colors to the list of variables as you move through the next steps and notice colors you may not have realized you were relying on!

 

3. Make dark mode color alternatives

The instructions below assume your default color mode is light mode. If your default is dark mode, then you’ll need to use prefers-color-scheme: light instead of prefers-color-scheme: dark.

All right, now’s the time to bring on the dark mode! I started with switching over easy-to-see elements, such as background colors, text colors, and big buttons. This could look something like this:

View Code
<!DOCTYPE html>
<html>
  <head>
    <style>
      /* Light mode colors */
      html {
        --button-text: white;
        --default-button-blackground: green;
        --override-button-background: black;
      }

      /* Dark mode colors */
      @media (prefers-color-scheme: dark) {
        html {
          --button-text: black;
          --default-button-blackground: lightgreen;
          --override-button-background: whitesmoke;
        }
      }

      .button {
        background-color: var(--default-button-blackground);
        border: none;
        color: var(--button-text);
        cursor: pointer;
        display: inline-block;
        font-size: 16px;
        margin: 4px 2px;
        padding: 15px 32px;
        text-align: center;
        text-decoration: none;
      }

      .override-background {
        background-color: var(--override-button-background);
      }
    </style>
  </head>

  <body>
    <h2>Button Colors Example</h2>

    <p>Check out my two buttons below!</p>

    <button class="button">Here's my button!</button>
    <button class="button override-background">Here's my override button!</button>
  </body>
</html>

To see the button colors switch between light and dark mode automatically, go into your computer or phone’s settings and actually switch your device from one mode to another and back and forth!

Here’s some quick notes about my example above:

  • I intentionally made the example colors formal HTML color names for ease of reading, but you can use rgb colors (rgb(144, 238, 144)), hex colors (#90ee90), or rgba colors (rgba(144, 238, 144, 0.85))
  • The @media (prefers-color-scheme: dark) block must be placed after the html block that defines the colors
  • If there’s a color in the html block that doesn’t need to have a dark mode alternative, then you don’t need to duplicate that line… just leave it out of the @media (prefers-color-scheme: dark) block entirely
  • The colors you select for dark mode may not be perfect when you start selecting them, but that’s okay; you’ll refine those colors in the next step
  • The prefers-color-scheme: dark media query is pretty widely supported these days, but it’s not supported everywhere; feel free to check on your favorite browsers here: caniuse.com

As stated earlier, as you go, you may end up finding more colors that need to be added that you didn’t even realize you were using. As I was creating this example, I realized I was depending on the default background of my page being white and the default text being black. So, I had to add a CSS section for the body, and add variables for the background and text of the body:

View Code
<!DOCTYPE html>
<html>
  <head>
    <style>
      /* Light mode colors */
      html {
        /* Button colors */
        --button-text: white;
        --default-button-blackground: green;
        --override-button-background: black;

        /* Body colors */
        --background-color: white;
        --text: black;
      }

      /* Dark mode colors */
      @media (prefers-color-scheme: dark) {
        html {
          /* Button colors */
          --button-text: black;
          --default-button-blackground: lightgreen;
          --override-button-background: whitesmoke;

          /* Body colors */
          --background-color: dimgrey;
          --text: white;
        }
      }

      body {
        background-color: var(--background-color);
        color: var(--text);
      }

      .button {
        background-color: var(--default-button-blackground); /* Default color */
        border: none;
        color: var(--button-text);
        cursor: pointer;
        display: inline-block;
        font-size: 16px;
        margin: 4px 2px;
        padding: 15px 32px;
        text-align: center;
        text-decoration: none;
      }

      .override-background {
        background-color: var(--override-button-background);
      }
    </style>
  </head>

  <body>
    <h2>Button Colors Example</h2>

    <p>Check out my two buttons below!</p>

    <button class="button">Here's my button!</button>
    <button class="button override-background">Here's my override button!</button>
  </body>
</html>
 

4. Test out your new color schemes and make sure text, buttons, etc show up clearly

Okay, now that we’ve got the basics of dark mode set up, it’s time for us to test! There’s no examples for this step, but when implementing dark mode on your websites, make sure to walk through each page (or at least each “style” of page – blog post, photo gallery, contact form, embedded video page, embedded PDFs, etc) to make sure there’s no elements that show up vastly different than you’d imagine.

You may continue to come across colors you didn’t know you were using. Some of the elements that caused me to have to go back to Step 2/3 and/or do additional research were:

 

Tables

I had to add the following to my table CSS to ensure that the text of the words changed color appropriately:

View Code
/* I needed to add this block */
.table-hover tbody tr:hover td, .table-hover tbody tr:hover th {
  color: var(--body-text);
}

table {
  display: block;
  overflow-x: auto;
}

tbody {
  border-top: solid 0.0625rem var(--table-border);
  color: var(--body-text); /* I needed to add this line */
}

/* I needed to add this block */
thead {
  color: var(--body-text);
}
 

Forms

I realized that I had to override the default input box background and text color by adding both the block below and corresponding colors:

View Code
.form-control {
  background: var(--form-input-background);
  color: var(--form-typed-text);

  &:focus {
    background: var(--form-input-background);
    color: var(--form-typed-text);
  }
}
 

reCAPTCHAs

Google’s reCAPTCHA is defaulted to a white background, which is perfect for a light mode. However, they also offer a dark mode option with an almost-black background. The issue is, we cannot simply change the color using CSS, like we’re changing the other elements. Instead, we need to completely ask for a different reCAPTCHA from Google. So instead of making a change in CSS, for this we’ll make a change via Javascritp:

View Code
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  document.getElementById('recaptcha').setAttribute('data-theme', 'dark')
};

Then, simply add an id to the reCAPTCHA HTML:

View Code
<!-- The line below wouldn't be the full HTML line, but it shows the gist of adding the `id` to the element -->
<div id="recaptcha" class="g-recaptcha form-recaptcha"></div>

To see this work, you’ll need to actually change the mode of your device, and then refresh the page. This is because instead of changing the element via CSS, we’re putting in a completely different element entirely.

 

Pagination Bar

I realized that a lot of my pagination bar was using default colors, e.g. hover background and text color, disabled background color, etc. So, I needed to add explicitly set variables for that:

View Code
/* I needed to add this block */
.page-item.disabled .page-link {
  background-color: var(--pagination-background);
  color: var(--pagination-text);
}

.page-link {
  background-color: var(--pagination-background);
  color: var(--pagination-text);

  /* I needed to add this block */
  &:hover {
    background-color: var(--pagination-background-hover);
    color: var(--pagination-text);
  }
}

This helped allow the backgrounds and text of the entire pagination bar to change modes!

 

Icons

Changing my icons to dark mode was the most challenging part by far. Because all of my icons are defined as SVG files and called as images, I had to actually create brand new SVGs of different colors, and then use the prefers-color-scheme: dark media query to determine which SVG to show.

For icons that didn’t require changing color on hover, this was simpler. I needed to add a different source image if prefers-color-scheme: dark.

View Code
<picture> <!-- I needed to add this line -->
  <!-- I needed to add this block -->
  <source
    media="(prefers-color-scheme: dark)"
    srcset="/assets/images/icons/icon-dark-mode.svg"
  >
  <img class="icon" src="/assets/images/icons/icon-light-mode.svg" alt="Icon">
</picture> <!-- I needed to add this line -->

However, if I wanted to change the color of the icon on hover, that was slightly more complex. I still needed to make copies of the SVGs, but now I need to have a copy for each mode as well as each mode when on hover:

Non-Hover Hover
Light Mode icon-light-mode-non-hover.svg icon-light-mode-hover.svg
Dark Mode icon-dark-mode-non-hover.svg icon-dark-mode-hover.svg

And then from the HTML, we’re actually going to add each of the four SVGs all at once:

View Code
<img class="icon icon-light-mode-non-hover" src="/assets/images/icons/icon-light-mode-non-hover.svg" alt="Icon">
<img class="icon icon-light-mode-hover" src="/assets/images/icons/icon-light-mode-hover.svg" alt="Icon">
<img class="icon icon-dark-mode-non-hover" src="/assets/images/icons/icon-dark-mode-non-hover.svg" alt="Icon">
<img class="icon icon-dark-mode-hover" src="/assets/images/icons/icon-dark-mode-hover.svg" alt="Icon">

But, we actually don’t want to show all four SVGs at the same time, so we’re going to use those HTML classes withCSS to conditionally show and hide icons!

View Code
.icon {
  &:hover {
    .icon-light-mode-non-hover {
      display: none;
    }
    .icon-light-mode-hover {
      display: inline;
    }
    .icon-dark-mode-non-hover {
      display: none;
    }
    .icon-dark-mode-hover {
      display: none;
    }
  }

  .icon-light-mode-non-hover {
    display: inline;
  }
  .icon-light-mode-hover {
    display: none;
  }
  .icon-dark-mode-non-hover {
    display: none;
  }
  .icon-dark-mode-hover {
    display: none;
  }
}

@media (prefers-color-scheme: dark) {
  html {
    .icon {
      &:hover {
        .icon-light-mode-non-hover {
          display: none;
        }
        .icon-light-mode-hover {
          display: none;
        }
        .icon-dark-mode-non-hover {
          display: none;
        }
        .icon-dark-mode-hover {
          display: inline;
        }
      }

      .icon-light-mode-non-hover {
        display: none;
      }
      .icon-light-mode-hover {
        display: none;
      }
      .icon-dark-mode-non-hover {
        display: inline;
      }
      .icon-dark-mode-hover {
        display: none;
      }
    }
  }
}

As you can see, we now can use the &:hover property paired with the HTML classes to determine which icons are shown and which are hidden. Voilá! I’m going to be super honest, when I found that solution, I was SUPER proud of myself.

 

Conclusion

At the end of the day, showing snippets of my code and simple examples isn’t going to help you solve all your edge cases. This is a great place to get started, but as you go, you may run into challenges that will let you learn more about your code and CSS.

But even if you do start to get stumped on those challenges, I would encourage you not to give up on dark mode yet. It may take a while, and you may have a lot of little changes here or there (implementing dark mode on my site took two pull requests and the biggest PR had 62 files changed), but dark mode will really help your readers during all hours of the day/night, make your website more accessible, and make your website look cool and hip. Personally, I am proud that I’m able to offer my readers a viable light mode and dark mode.