Menu

Making a theme selector

How to make a theme selector for your website, and why you might want to have more than one theme.

It’s weirdly hard to do this

If you want a web page in only one theme, that’s relatively straightfoward. If you want to support dark mode, the use of @media (prefers-color-scheme: dark) with CSS custom properties isn’t too bad. But adding a theme selector requires… JavaScript.

The JS

Download and install wavebeem-theme-select.mjs. The source code is commented and less than a hundred lines.

<script type="module" src="/wavebeem-theme-select.mjs"></script>

The HTML

I made a custom web component that wraps a <select> tag and updates localStorage and the [data-theme] on the <html> element.

<wavebeem-theme-select>
  <select autocomplete="off">
    <option value="" disabled selected>Select theme...</option>
    <option value="auto">Auto</option>
    <option value="light">Light</option>
    <option value="dark">Dark</option>
  </select>
</wavebeem-theme-select>

You can add other elements inside <wavebeem-theme-select> like <label> for example. The mandatory parts are the <option> elements and the specific values "", "auto", "light", and "dark", since the <wavebeem-theme-select> relies on them.

The CSS

I highly recommend using CSS custom properties to set color variables.

/* Styles for before the component has loaded */
:root,
/* Styles for after the component has loaded */
:root[data-theme="light"] {
  --color-background: #fff;
  --color-text: #222;

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

/* Styles when dark mode set by JS */
:root[data-theme="dark"] {
  --color-background: #333;
  --color-text: #ccc;
}

For best compatibility, you should also include the dark mode styles in a media query, so that dark mode is applied when the JS doesn’t load. Granted, it’s tedious to repeat the colors in a separate media query, so you may want to avoid this if you don’t like keeping these things synced up.

/* Styles when dark mode is set by browser */
@media (prefers-color-scheme: dark) {
  :root {
    --color-background: #000;
    --color-text: #fff;
  }
}

Bonus round: theme-color for mobile

There’s also the theme-color meta property which mobile browsers use to color the browser UI. It’s rarely used on desktop, though.

<meta name="theme-color" content="#0000ff" />
<meta
  name="theme-color"
  content="#0088ff"
  media="(prefers-color-scheme: dark)"
/>

You can use the media property to contain a CSS @media query in order to automatically apply to system dark mode. If you need to support user selected themes, then you can use JavaScript to update the content value of the <meta> tag.

Accessibility is complicated

Having a light theme and a dark theme is important for accessibility. Dark themes are useful for low light situations, or for people who are sensitive to light! However, I have a hard time reading dark themes because the text appears more blurry to my vision. When possible, both is best. And you really don’t need a complicated theme switcher like I have. Using the system dark mode setting via media queries is more than enough for most personal sites.


Stay up to date

Subscribe to my RSS feed to stay up to date with my posts.

Let's talk

My blog doesn't support comments, but you're welcome to contact me to talk about this post.

Browse related posts