Website Themes and Color Schemes

Posted on February 1, 2022
Takes about 7 minutes to read

Getting into the mode

Before we begin, I'd like to preface this article with the following resources that were helpful guides on my theming quest. They explain a lot of the intricacies of setting up dark mode and I recommend reading them before my own.

Read those already? Skimmed them a fair amount at least? Fantastic. Let's jump into the theming details for this website.

Range of styles

My first go-around with theme switching was handled with an HTML range input (dubbed theme slider). Each input value correlated to a CSS ruleset. Interacting with the theme slider did a couple things:

  1. Set a data-theme attribute on the <html> element.
  2. Saved the theme value to the browser's local storage to be referenced on subsequent site visits.

Each theme changed the site colors set in CSS custom properties. I've simplified the code in these examples for the sake of brevity.

[data-theme="1"] {
  /* dark theme */
  --color-text: papayawhip;
  --color-bg: midnightblue;
}

/* ...rulesets for themes 2 through 4... */

[data-theme="5"] {
  /* light theme */
  --color-text: darkslategray;
  --color-bg: lightsalmon;
}

The min and max attributes on the theme slider were set to 1 and 5 respectively, allowing five different themes. If the slider had not yet been moved, a theme was applied to the site based on color scheme preferences in a user's system settings. By default, color values were set to CSS custom properties. These values were then updated for dark mode within a prefers-color-scheme media query.

In the following example, you'll notice that the base default colors are the same values in data-theme="5" and then get updated to match data-theme="1" for the dark color scheme preference.

:root {
  /* same values used in theme 5 */
  --color-text: darkslategray;
  --color-bg: lightsalmon;
}

@media (prefers-color-scheme: dark) {
  /* same values used in theme 1 */
  :root {
    --color-text: papayawhip;
    --color-bg: midnightblue;
  }
}

This first iteration felt limited. Preferred color scheme values were tied to specific themes (1 for dark, 5 for light/no-preference) and disconnected from the values sandwiched in between. It was a fine start but left me wondering about other ways to handle these preference settings.

Decoupling scheme and theme

When I first migrated over to 11ty and added more pages to this site, the theme switcher was still only accessible on the homepage. While moving this component into a global layout, I was hit with some swell brain activity:

Instead of relating light and dark mode settings to specific theme values on the slider, they could alter each theme as variants.

🤯

Here's what I came up with. The theme slider works the same as before but now has a new neighbor: a color scheme toggle button. This button sets a light or dark version of the current theme. My selection of colors may be somewhat arbitrary and subjective, but I tried pairing palettes that complement one another.

Open CodePen demo

What I like about this theming model is that it welcomes future variants based on other user preferences and system settings. For instance, introducing high and low contrast styles for each theme using the prefers-contrast media query.

Redundant rulesets

One minor issue is that the same set of styles need to be declared twice for an initial theme to handle dark mode as both a system setting and user-selected preference. Since I'm using Sass, I've abstracted the values into a mixin to avoid the repetition. Below is a reduced example; Review the SCSS tab in the CodePen above for the full code.

@mixin color-scheme-dark {
  --color-text: papayawhip;
  --color-bg: midnightblue;
}

:root {
  --color-text: darkslategray;
  --color-bg: lightsalmon;

  @media (prefers-color-scheme: dark) {
    &:not([data-color-scheme]) {
      @include color-scheme-dark;
    }
  }

  &[data-color-scheme="dark"] {
    @include color-scheme-dark;
  }
}

A hefty thanks to Andy Bell's article for its usage of the :not selector. This ensures that the system settings do not override the user-selected color scheme. It also helped reduce some of the redundant code.

Keyboard combo control

Keyboard navigation and interaction for these two elements works as one might suspect. The button toggles between dark and light mode. The range input updates the theme value. It's debatable that they are best left like that. However, I wanted to explore a version that combined these element interactions; A way to cycle through themes and toggle their light/dark variants as a single control.

Theme status

One last feature is the usage of the status role, another gem from Andy's article. An HTML element with a role="status" attribute is grouped next to each control. Although visually hidden, when the content inside these containers changes, assistive technology will relay that update back to us.

Something I enjoy about the latter bullet point is that it reveals the actual theme names which are based on ice cream flavors. Without inspecting code, it's currently the only path to this discovery. What's cooler than being cool? Ice cream.

Ending theme

Thanks for joining while I recounted the tale of my website's first theming trial and its follow-up adventure. Some of the patterns here may change over time but this has been a blast putting together. I'm no champion of color, but I think these are some good lookin' themes.

If you have your own unique implementation or favorites out there on the wild web, please share! Max Böck and Josh Comeau have beautiful theme switchers and wrote detailed articles about their journeys. Definitely worth the read:

Back to all blog posts