Note7 minute read

Switching screen theme with pure CSS (Dark/Light Mode)

A simple light and dark theme implementation with CSS, keeping the interface predictable.

  • frontend
  • css
  • accessibility

We know the JS way

But what if we didn't use scripts to switch our app themes?

The path is a relationship between cascade and well-targeted selectors.

Let's start from the top:


HTML

The first element in the tree is a checkbox input.

Its sibling below is our app container. That's what gets restyled when the theme changes.

Inside it, we have a label tied to the input above, inside a div that acts as the transition area and serves as our theme toggle button.

<body>
  <input type="checkbox" id="theme-switcher">
  <div id="app-container">
    <div class="theme-switcher-area">
      <label for="theme-switcher" class="theme-switcher-button">
      </label>
    </div>
    <h1>Mudando tema com CSS Puro</h1>
    <p>O texto fica em contraste 
    com o fundo</p>
  </div>
</body>

CSS

In the styles, we apply resets and declare the color variables for the theme:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --light: #cccccc;
  --dark: #151515;
}

We hide the input since we'll use its label as the trigger.

#theme-switcher {
  display: none;
}

We declare the app container properties. It fills the screen, has a light background and dark text, and is a flex container. The last part is optional, just to center the demo content on screen.

#app-container {
  height: 100vh;
  background: var(--light);
  color: var(--dark);
  font-family: monospace;
  font-size: 1.5rem;
  transition: 0.3s;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

We declare the area the button slides through, absolutely positioned at the top:

.theme-switcher-area {
  border: 1px solid var(--light);
  background: var(--dark);
  border-radius: 2rem;
  width: 4.5rem;
  height: 2.5rem;
  padding: 0.2rem;
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
}

The button itself uses a dashed border, creating a sun-ray effect for the light theme.

.theme-switcher-button {
  position: relative;
  display: block;
  background: #f1aa02;
  border-radius: 50%;
  width: 2rem;
  height: 2rem;
  border: 2px dashed var(--dark);
  transition: 0.3s;
}

Finally, a ::after pseudo-element on the button. It's a smaller circle than the original element, becoming a shadow that turns the trigger into a moon in dark mode. So its initial opacity is 0.

.theme-switcher-button::after {
  position: absolute;
  width: 80%;
  height: 80%;
  content: "";
  background: var(--dark);
  border-radius: 50%;
  opacity: 0;
  transition: 0.3s;
}

Here's the trick

Since our input is the first element in the tree, we can use the :checked pseudo-class with the right selectors to restyle anything below it.

When it's checked, these rules apply.

Starting with the trigger itself, turning the sun into a moon. We remove the border that created the ray effect and slide the button to the right.

#theme-switcher:checked + #app-container .theme-switcher-button {
    transform: translateX(100%);
    border: none;
}

Next, we change the shadow opacity on ::after to form a crescent moon as the button switches.

#theme-switcher:checked + #app-container .theme-switcher-button::after {
    opacity: 1;
}

Last, and for the main effect, we flip the background and text color on the app container:

#theme-switcher:checked + #app-container {
    background: var(--dark);
    color: var(--light);
}

And there it is.

Theme switch demo using only CSS


This tutorial is just the start. You can extend it with prefers-color-scheme, localStorage persistence, and more elaborate sun/moon button animations.