DEV Community

Uduakabaci Udofe
Uduakabaci Udofe

Posted on

How to achieve dark/light mode with CSS.

If you have ever written CSS for a large web app then you know how hard it is to manage CSS. Add that to the increasing need to support dark and light modes in your app and you’ll have an overwhelming app starring you in the face. It helps to have methodologies and pre-processors handy but even with these tools, CSS can easily balloon into a monster code-base that's very hard to manage if not properly structured from the get-go.

In this guide, I'll introduce you to a simple system i use to manage my CSS and how you can absorb it into your current front-end workflow. We'll start with a brief introduction to methodologies and preprocessors and why you should pick up one if you haven’t already.

Why Do We Need CSS Methodologies?

When it comes to writing CSS, I think it’s better to avoid selecting tags or even an element’s descendant because the HTML structure can change in the future. A better option is to split the HTML into independent components, style them with classes, and then compose them to achieve the desired interface, and here is where CSS methodologies come in. CSS methodologies are formal, documented systems for writing CSS in a way that allows us to develop, maintain and scale the front-end as a set of small, isolated modules.

CSS methodologies provide us with structures and mental models to manage CSS efficiently. With CSS methodologies, we can embrace the whole DRY (don’t repeat yourself) ideology easily because our CSS will be divided into isolated modules which makes styling a breeze and repetition kind of hard.

Why Do We Need CSS Preprocessors?

Whereas methodologies provide us with systems to manage our CSS, preprocessors such as SASS, LESS, and stylus provide tools to implement these in a way that is easy to understand and maintain. There are a few methodologies and preprocessors to choose from, but for this guide, I'll be using the BEM methodology because it is relatively easy to pick up and it is very intuitive. I’ll also be using SASS as my preprocessor of choice because of its mass appeal.

A Better Way To Structure CSS

The first step towards building a scalable and maintainable system is to group the primary values. Primary values are values that multiple parts of the system depend on, for example, colors, font families, and font sizes. If multiple components of the system rely on a value, it makes sense to isolate the value and store it somewhere and then reference that value from the dependent components instead of hard-coding the value into these components. So that in an event of a change, we will only update one part of our system and have the change reflected in all dependent components.

When grouping the primary values, we will store these values in CSS variables and reference these variables in our component. What we want to do is to pick out the primary colors and fonts and store them in CSS variables with explainable names. It’s easier if we have a UI to look at but if we don’t, then we’ll need to make these hard design decisions ourselves.

Some designs use different fonts for different hierarchies and different colors for different messages/text, so it makes sense to understand what we are working with. When naming our font variables, it’s best to name them in terms of their use-case instead of some generic name, the same thing with colors. We want to abandon names like --font-ubuntu, --color-red for names like --headline-font, --main-accent-color as these names explain the roles of each font and color in our system. This way, we understand what each color and font does at glance.

With everything we’ve said so far, our codebase should look more like this.

:root {
  --main-accent0: hsl(165, 100%, 50%);
   /* lighter version for hovers */
  --main-accent1: hsl(165, 100%, 90%); 
  --headline-font: Ubuntu;
}

/* then in our call to action we can do like this*/
.button {
   background-color: var(--main-accent0);
   font-family: var(--headline-font);
   &:hover {
    background-color: var(--main-accent-1);
   }
}
Enter fullscreen mode Exit fullscreen mode

How To Structure CSS For Theme Switching

When it comes to themes (dark mode/light mode), there are a couple of ideas I know of: one way is to put the dark and light theme variables in their separate stylesheets and load them when the user needs them. I do not like this approach because the browser will have to fetch the themes from the server, and for servers with high latency, users with bad network speed, or even users using our app offline, our web app might not work smoothly.

My preferred approach is to have all the variables in one stylesheet, split them up into classes, and then toggle these classes depending on what mode we want to achieve. Here is what I mean.

/*main.scss*/


.theme {
  &__light {
    --high-contrast-bg: hsl(194, 2%, 93%);
    --high-contrast-text: hsl(194, 2%, 28%);
  }
  &__dark {
    --high-contrast-bg: hsl(194, 2%, 48%);
    --high-contrast-text: hsl(194, 2%, 98%);
  }
}

.card {
  padding: 20px;
  background-color: var(--high-contrast-bg);
  color: var(--high-contrast-text);
}
Enter fullscreen mode Exit fullscreen mode
<!-- index.html -->
 <body class="theme theme__light">
    <div class="card">
    <div class="card__header">
      header
    </div>
    <div class="card__body">
      body
    </div>
    <button class="theme-switcher">switch to <span class="theme-switcher__current-mode">dark</span> mode</button>
  </div>
 </body>
Enter fullscreen mode Exit fullscreen mode

Here is a Javascript snippet to help us achieve that.

document.addEventListener("DOMContentLoaded", () => {
  const theme = document.querySelector(".theme");
  const button = document.querySelector(".theme-switcher");
  const mode = document.querySelector(".theme-switcher__current-mode");
  button.addEventListener("click", () => {
    theme.classList.remove("theme__dark", "theme__light");
    if (mode.innerText == "dark") {
      theme.classList.add("theme__dark");
      mode.innerText = "light";
    } else {
      theme.classList.add("theme__light");
      mode.innerText = "dark";
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

Thank you for reading this guide, I hope you’ve learned a thing or two. If you have a question or a suggestion for this guide, please don’t hesitate to send ‘em in.

Discussion (15)

Collapse
hanz profile image
Hanz

Lazy front-end developers be like:
filter: invert(1);

Collapse
zakarialaoui10 profile image
ZAKARIA ELALAOUI

images love your comment hhh

Collapse
ksengine profile image
Kavindu Santhusa • Edited on

This method converts links from blue to yellow.
Take a look at this solution.
dev.to/ksengine/comment/1kclh

Collapse
nielskersic profile image
Niels Kersic

If you only have a few themed variables, it's okay to store them in a single CSS file like main.css. If your light and dark themes are more elaborate, I would recommend splitting them into separate files.
You mention that you don't like this approach because of slow connections and high latency, but in cases where the user's connection or device is slow, it's better to only send them the theme they need, instead of both themes.

Collapse
uduakabaci profile image
Uduakabaci Udofe Author

All these are valid arguments, but this is what works for me. I love having the primaries in one file as it makes maintenance very easy for me. I've never needed to split my variables into separate files, so splitting seems a bit overkill, but I understand what you are getting at.
Thanks for pointing this out.

Collapse
ksengine profile image
Kavindu Santhusa
Collapse
joeattardi profile image
Joe Attardi

You can also use the prefers-color-scheme media query and automatically set the theme based on the OS settings!

Collapse
uduakabaci profile image
Uduakabaci Udofe Author

Thanks for pointing this out. I'm not sure why i left it out but you achieve that with this snippet.
document.addEventListener("DOMContentLoaded", () => {
const theme = document.querySelector(".theme");
const button = document.querySelector(".theme-switcher");
const mode = document.querySelector(".theme-switcher__current-mode");
button.addEventListener("click", () => toggleTheme(mode.innerText == "dark"));

const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
// check for browser support
if (darkModeMediaQuery) {
darkModeMediaQuery.addListener((e) => {
const darkModeOn = e.matches;
toggleTheme(darkModeOn);
});
}

function toggleTheme(isDarkTheme) {
theme.classList.remove("theme_dark", "themelight");
if (isDarkTheme) {
theme.classList.add("theme
dark");
mode.innerText = "light";
} else {
theme.classList.add("theme
_light");
mode.innerText = "dark";
}
}
});

Collapse
serhiityshchenko profile image
Serhii Tyshchenko

If you're following BEM methodology you should use -- for modifiers, ex theme--light

Collapse
uduakabaci profile image
Uduakabaci Udofe Author

haha. Thanks for pointing that out. theme--light is the correct thing to do.

Collapse
bhardwajzone profile image
Shubham Bhardwaj
Collapse
ekimcem profile image
Ekim Cem Ülger

Thank you so much !

Collapse
uduakabaci profile image
Uduakabaci Udofe Author

i'm glad you like it.

Collapse
oviecodes profile image
Godwin Alexander

Nice post bro

Collapse
uduakabaci profile image
Uduakabaci Udofe Author

Thanks, man.