﻿---
title: "Multi-Theme Blog Design in Hexo: Theme Switching with CSS Variables and the :where Selector"
date: 2025-11-18
excerpt: How to manage multiple blog themes with CSS variables and the :where selector, including Shiki syntax highlighting theme switching and a FOUC fix.
tags:
  - Hexo
  - Web
  - Theme
  - Catppuccin
  - Shiki
  - Ricing
  - Blog
  - CSS
  - JavaScript
updated: 2026-07-08 21:23:26
lang: en
i18n:
  cn: /web_theme
  translation: 2
---

<script type="module" src="/js/components/tab.js"></script>

This post shows how to manage multiple blog themes with CSS variables and the `:where` selector, covering theme switching for Shiki code highlighting and a fix for the flash of unstyled content (FOUC)

<!--more-->

## Preview

<video autoplay loop muted playsinline>
  <source src="https://assets.vluv.space/blog_themes.webm" type="video/webm">
</video>

## Setup Multi-Themes with CSS Variables

The core of multi-theme support is **decoupling** color values from CSS rules:

- Set a `{css} data-theme` attribute on the HTML root element to identify the current theme (such as `nord`, `tokyo_night`); when switching themes, update this attribute dynamically
- For each `{css} data-theme` value, define CSS variables with the same names:
  - `{css} :where([data-theme=nord]) { --red: #d20f39; }`
  - `{css} :where([data-theme=tokyo_night]) { --red: #f7768e; }`
- Component color styles all reference these variables via `var(--color-var)`

### Define Color Variables

To define theme-specific variables, use the `:where` selector[^1]; it is also worth setting the `{css} color-scheme`[^2] property, which tells the browser whether the current theme is light or dark and affects the system default styles of scrollbars and form controls

```css default.css
:where([data-theme="nord"]) {
	color-scheme: light;
	--red: #d20f39;
}

:where([data-theme="tokyo_night"]) {
	color-scheme: dark;
	--red: #f7768e;
}
```

With these CSS rules in place, page elements pick up the color values of the current theme dynamically.

For example, in the page below, the `{css} h1` element uses the `--red` value from the `nord` theme, `#d20f39`. When the user switches to the `tokyo_night` theme, the `{css} h1` element picks up the new `--red` value, `#f7768e`

```html index.html
<!-- apply nord theme -->
<html data-theme="nord">
  <head>
    <link rel="stylesheet" href="color.css" />
  </head>
  <body>
    <h1 style="color: var(--red)">Hello, World!</h1>
  </body>
</html>
```

### Follow System Theme

There are two ways to implement a "follow system theme" option:

<x-tabs>

<x-tab title="CSS" active>

The CSS approach works like this: first define the color variables for `{css} data-theme="system"`, defaulting to the light theme. After that rule, use a `{css} @media (prefers-color-scheme: dark)` media query to detect the system's dark preference; if the system prefers dark, redefine the dark theme values for `{css} :where([data-theme="system"])`, overriding the earlier light values. When the user chooses "follow system", simply set `{css} data-theme` to `"system"` and the browser applies the right color variables based on the system preference. The drawback of this approach is the redundant variable definitions, which raise the maintenance cost

 ```css color.css
/* ... light ... */
:where([data-theme="system"]) {
	color-scheme: light dark;
	--rosewater: #dc8a78;
}

/* ... night ... */
@media (prefers-color-scheme: dark) {
	:where([data-theme="system"]) {
		--rosewater: #f5e0dc;
	}
}
```

</x-tab>

<x-tab title="JavaScript">

With JavaScript, you can set the `{css} data-theme` attribute to a concrete theme name based on the system preference. The `resolveTheme` function checks the current theme value: if it is `"system"`, it detects dark mode via `window.matchMedia("(prefers-color-scheme: dark)")` and returns the corresponding theme name (such as `"mocha"` or `"nord"`); for any other fixed theme, it returns that theme name directly. The `applyTheme` function applies the resolved theme to the HTML element.

```js theme-selector.js
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

function resolveTheme(theme) {
	return theme === "system"
		? colorSchemeMediaQuery.matches
			? "mocha"
			: "nord"
		: theme;
}

function applyTheme(theme) {
	const html = document.documentElement;
	const resolvedTheme = resolveTheme(theme);
	html.setAttribute("data-theme", resolvedTheme);
	html.classList.remove("night", "light");
	html.classList.add(THEME_MAP[resolvedTheme]);
}
```

</x-tab>

</x-tabs>

#### Theme Persistence and Responsiveness

To round out the theme switching experience, two problems remain:

**Persistence**: the theme the user picks should survive page reloads and browser restarts. Save the choice locally with `localStorage` and restore it on the next visit.

```js theme-selector.js
// [!code word:persist]
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

// Read the user's chosen theme from localStorage
function getThemePreference() {
	// [!code ++]
	const stored = localStorage.getItem(STORAGE_KEY); // [!code ++]
	return stored && stored in THEME_MAP ? stored : DEFAULT_THEME; // [!code ++]
} // [!code ++]

// Called when the user picks a theme, with persist=True
function applyTheme(theme, persist) {
	const html = document.documentElement;
	const resolvedTheme = resolveTheme(theme);
	html.setAttribute("data-theme", resolvedTheme);
	html.classList.remove("night", "light");
	html.classList.add(THEME_MAP[resolvedTheme]);
	if (persist) {
		// [!code ++]
		localStorage.setItem(STORAGE_KEY, theme); // [!code ++]
	} // [!code ++]
}
```

**Responsiveness**: when the user has picked "follow system" and the system preference changes (say, from light mode to dark mode), the page should respond and update the theme immediately.

```js
colorSchemeMediaQuery.addEventListener("change", () => {
	if (getThemePreference() === "system") {
		applyTheme("system");
	}
});
```

### Transparent Colors

> [!example]-
>
> Some components need semi-transparent colors. This paragraph, for example, uses the Callout component. A simple but inelegant way to handle this:
>
> ```css
> :where([data-theme="nord"]) {
> 	--red: #d20f39;
> 	--red-10: rgba(210, 15, 57, 0.1); /* 10% opacity */
> 	--red-50: rgba(210, 15, 57, 0.5); /* 50% opacity */
> 	--green: #40a02b;
> 	--green-10: rgba(64, 160, 43, 0.1);
> 	--green-50: rgba(64, 160, 43, 0.5);
> 	/* other colors */
> }
> ```

If transparent colors are used widely, the variable count multiplies; this approach takes real effort and is costly to maintain. A better option is CSS relative color syntax[^3] for defining transparent colors:

```css
color-function(from origin-color channel1 channel2 channel3)
color-function(from origin-color channel1 channel2 channel3 / alpha)

/* color space included for color() functions */
color(from origin-color colorspace channel1 channel2 channel3)
color(from origin-color colorspace channel1 channel2 channel3 / alpha)
```

For the red in the example above, you can reuse the `--red` variable directly to define a semi-transparent color:

```css
/* red background at 50% opacity */
background-color: hsl(from var(--red) h s l / 0.5);
```

Browser compatibility[^4] is so-so, but for a personal blog it hardly matters.

## Avoid FOUC

One way to avoid a flash[^5] on initial render: insert an IIFE (Immediately Invoked Function Expression) script into `{html} <head>`, guaranteeing the `{css} data-theme` attribute is set before the page renders

```html
<html lang="zh-CN" data-theme="mocha" class="night">
    <head>
        <script>
        (function() {
          var THEME_MAP = {
            mocha: "night", macchiato: "night",
            nord: "light", nord_night: "night",
            tokyo_night: "night", latte: "light"
          };
          var stored = localStorage.getItem("themePreference");
          var theme = stored ? stored : "system";
          var html = document.documentElement;
          var resolvedTheme = theme === "system"
            ? window.matchMedia("(prefers-color-scheme: dark)").matches ? "mocha" : "nord"
            : theme;
          html.setAttribute("data-theme", resolvedTheme);
          html.classList.add(THEME_MAP[resolvedTheme]);
        })();
        </script>
    </head>
</html>
```

## Special Components

### Code Highlighting

For code highlighting, this blog uses Shiki for static rendering. Shiki accepts multiple themes. For example:

```js
const themes = {
	light: "catppuccin-latte",
	dark: "catppuccin-mocha",
	tokyo: "tokyo-night",
};

const code_html = highlighter.codeToHtml(code, {
	lang: options.lang || "",
	themes: themes,
	transformers: enableTransformers ? SUPPORTED_TRANSFORMERS : [],
});

/* Generated Span Be Like  */
<span style="color:#1E66F5;--shiki-dark:#89B4FA;--shiki-tokyo:#7AA2F7">
	name
</span>;
```

It is easy to see that the `{css} data-theme` attribute, combined with CSS variables, gives us multi-theme color support:

```css shiki.css
code span {
	font-style: var(--shiki-light-font-style);
	font-weight: var(--shiki-light-font-weight);
}
:is([data-theme="tokyo_night"]) {
	code span {
		font-style: var(--shiki-tokyo-font-style) !important;
		font-weight: var(--shiki-tokyo-font-weight) !important;
		color: var(--shiki-tokyo) !important;
	}
}
```

> [!TIP]- Further Optmization
>
>With more themes, the generated span tags become bloated. The ideal solution is to generate a dedicated class for each color. Shiki provides the [transformerstyletoclass](https://shiki.style/packages/transformers#transformerstyletoclass) transformer, which generates HTML like this
>
>```html
><pre
>  class="shiki shiki-themes vitesse-dark vitesse-light __shiki_9knfln"
>  tabindex="0"
>>
>    <code>
>        <span class="line">
>          <span class="__shiki_14cn0u">console</span>
>          <span class="__shiki_ps5uht">.</span>
>          <span class="__shiki_1zrdwt">log</span>
>          <span class="__shiki_ps5uht">(</span>
>          <span class="__shiki_236mh3">'</span>
>          <span class="__shiki_1g4r39">hello</span>
>          <span class="__shiki_236mh3">'</span>
>          <span class="__shiki_ps5uht">)</span>
>        </span>
>    </code>
></pre>
>```
>
>Use the `{js} transformerStyleToClass({ classPrefix: '__shiki_'}).getCSS()` API to obtain the corresponding CSS file; see [[shiki_style_to_class|my post on this]] for details
>
>```css auto_generated.css
>.__shiki_14cn0u {
>	--shiki-dark: #bd976a;
>	--shiki-light: #b07d48;
>}
>/* ... */
>.__shiki_9knfln {
>	--shiki-dark: #dbd7caee;
>	--shiki-light: #393a34;
>	--shiki-dark-bg: #121212;
>	--shiki-light-bg: #ffffff;
>}
>```

[^1]: The CSS `:where()` pseudo-class is used to apply the same style to all the elements inside the parentheses, at the same time. `:where()` always has 0 specificity. [CSS :where Pseudo-class](https://www.w3schools.com/cssref/sel_where.php)
[^2]: Common choices for operating system color schemes are "light" and "dark", or "day mode" and "night mode". When a user selects one of these color schemes, the operating system makes adjustments to the user interface. This includes [form controls](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms), [scrollbars](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scrollbars_styling), and the used values of [CSS system colors](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/system-color). [color-scheme - CSS | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/color-scheme)
[^3]: The [CSS colors module](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Colors) defines **relative color syntax**, which allows a CSS color value to be defined relative to another color. This is a powerful feature that enables easy creation of complements to existing colors — such as lighter, darker, saturated, semi-transparent, or inverted variants — enabling more effective color palette creation. [Using relative colors - CSS | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Colors/Using_relative_colors)
[^4]: [CSS Relative color syntax | Can I use... Support tables for HTML5, CSS3, etc](https://caniuse.com/css-relative-colors)
[^5]: A **flash of unstyled content** (FOUC, or flash of unstyled text) is an instance where a web page appears briefly with the browser's default styles prior to loading an external CSS stylesheet, due to the web browser engine rendering the page before all information is retrieved.
