Dark Light Theming Code in Astro

What

Setting separate syntax highlighting themes for dark and light themes on my Astro generated website.

March 2023 - This was for my old Astro 1.6 site, since then I re-wrote it and used the render both themes with Shiki and hide one solution.

Shiki’s Suggested Solutions

The default syntax highlighter Astro uses on markdown is Shiki. Shiki’s docs have 2 suggested ways of switching themes, neither of them were satisfactory to me.

The first suggestion is to use css-variables, which sounds ideal, however the css theme is just a few colours. The stylesheets and language definitions that make up syntax themes are more than just a few colours, this seems like a simplification.

The second suggestion is to render the code twice, once for each theme, hiding one with css. Trying to set that up requires messing with Astro’s markdown integration, it isn’t obvious how to achieve this. I don’t want to spend time messing around with that.

My Solution

Configure Astro to use Prism instead of Shiki for syntax highlighting.

json
// https://astro.build/config
export default defineConfig({
  markdown: {
    syntaxHighlight: "prism",
  },
});
json
// https://astro.build/config
export default defineConfig({
  markdown: {
    syntaxHighlight: "prism",
  },
});

Download 2 Prism themes, name them syntax-dark.css and syntax-light.css, add one of the css files in the head

html
<link id="syntax-highlight-css" rel="stylesheet" href="/syntax-dark.css" />
html
<link id="syntax-highlight-css" rel="stylesheet" href="/syntax-dark.css" />

Update my theme changing script to swap the stylesheets when the theme is toggled.

This script does a few things

ts
const dark = "dark";
const light = "light";
const key = "theme";
const cssId = "syntax-highlight-css";

activateTheme();

document.querySelectorAll('[id^="toggle-theme"]').forEach((element) => {
    element.onclick = () => {
        toggleTheme();
    };
});

/** Set the CSS to activate whatever theme is set in localStorage.theme or CSS media. */
function activateTheme() {
    const theme = getLocalTheme() ?? getCssTheme() ?? defaultTheme;
    if (theme === dark) {
        document.documentElement.classList.add(dark);
        document.getElementById(cssId).setAttribute("href", "/syntax-dark.css");
    } else {
        document.documentElement.classList.remove(dark);
        document
            .getElementById(cssId)
            .setAttribute("href", "/syntax-light.css");
    }

    // Clear local storage if user's computer settings match the current stored setting.
    // Only want to keep a setting persisted while it is differnt from the user's settings.
    if (theme === getCssTheme()) {
        localStorage.removeItem(key);
    }
}

/** Toggle the current theme and set it to persist if it does not match the computer setting. */
function toggleTheme() {
    const localTheme = getLocalTheme();
    const cssTheme = getCssTheme();
    const current = localTheme ?? cssTheme ?? defaultTheme;

    const theme = current === light ? dark : light;

    if (theme === cssTheme) {
        localStorage.removeItem(key);
    } else {
        localStorage.setItem(key, theme);
    }

    activateTheme();
}

function getCssTheme() {
    if (window.matchMedia("(prefers-color-scheme: dark)").matches) return dark;
    if (window.matchMedia("(prefers-color-scheme: light)").matches)
        return light;
    return null;
}

function getLocalTheme() {
    if (localStorage.getItem(key) === dark) return dark;
    if (localStorage.getItem(key) === light) return light;
    return null;
}
ts
const dark = "dark";
const light = "light";
const key = "theme";
const cssId = "syntax-highlight-css";

activateTheme();

document.querySelectorAll('[id^="toggle-theme"]').forEach((element) => {
    element.onclick = () => {
        toggleTheme();
    };
});

/** Set the CSS to activate whatever theme is set in localStorage.theme or CSS media. */
function activateTheme() {
    const theme = getLocalTheme() ?? getCssTheme() ?? defaultTheme;
    if (theme === dark) {
        document.documentElement.classList.add(dark);
        document.getElementById(cssId).setAttribute("href", "/syntax-dark.css");
    } else {
        document.documentElement.classList.remove(dark);
        document
            .getElementById(cssId)
            .setAttribute("href", "/syntax-light.css");
    }

    // Clear local storage if user's computer settings match the current stored setting.
    // Only want to keep a setting persisted while it is differnt from the user's settings.
    if (theme === getCssTheme()) {
        localStorage.removeItem(key);
    }
}

/** Toggle the current theme and set it to persist if it does not match the computer setting. */
function toggleTheme() {
    const localTheme = getLocalTheme();
    const cssTheme = getCssTheme();
    const current = localTheme ?? cssTheme ?? defaultTheme;

    const theme = current === light ? dark : light;

    if (theme === cssTheme) {
        localStorage.removeItem(key);
    } else {
        localStorage.setItem(key, theme);
    }

    activateTheme();
}

function getCssTheme() {
    if (window.matchMedia("(prefers-color-scheme: dark)").matches) return dark;
    if (window.matchMedia("(prefers-color-scheme: light)").matches)
        return light;
    return null;
}

function getLocalTheme() {
    if (localStorage.getItem(key) === dark) return dark;
    if (localStorage.getItem(key) === light) return light;
    return null;
}