Skip to content

Instantly share code, notes, and snippets.

@wvovaw
Last active February 7, 2024 13:47
Show Gist options
  • Save wvovaw/2c35a8cd0846f711c23034e85c74fbbf to your computer and use it in GitHub Desktop.
Save wvovaw/2c35a8cd0846f711c23034e85c74fbbf to your computer and use it in GitHub Desktop.
Nuxt SSR friendly color theme switcher

Nuxt 3 + vueuse + Pinia SSR friendly color theme switcher

  1. Declare theme cookie in the app.vue.
  2. Use HTML nuxt builtin to bind data-theme attribute (your may differ) to the cookie.theme.
  3. Create theme.store.ts Pinia store. cycle function cycles through the list of modes and sets current state in the theme cookie
  4. Create ThemeSwitcher.vue component.

This approach is universal, SSR friendly and easy adoptable.

Read more about useColorMode

<script setup lang="ts">
const expireDate = new Date();
expireDate.setFullYear(expireDate.getFullYear() + 1);
let cookie = useCookie<{
theme: string;
}>("theme", {
default: () => ({ theme: "" }),
expires: expireDate,
watch: "shallow",
});
addRouteMiddleware("theme-cookie-update", () => {
cookie = useCookie("theme");
}, { global: true });
</script>
<template>
<Html :data-theme="cookie.theme">
<Body>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</Body>
</Html>
</template>
/* Theme example */
:root {
--l-dark: 73, 64%, 4%;
--l-light: 65, 65%, 97%;
--l-primary: 69, 65%, 55%;
--l-secondary: 124, 65%, 73%;
--l-accent: 138, 66%, 62%;
--d-dark: 66, 63%, 3%;
--d-light: 74, 62%, 96%;
--d-primary: 69, 65%, 45%;
--d-secondary: 124, 65%, 27%;
--d-accent: 138, 66%, 38%;
}
html[data-theme="light"] {
--p-fg: var(--l-dark);
--p-bg: var(--l-light);
--p-primary: var(--l-primary);
--p-primary-contrast: var(--l-dark);
--p-secondary: var(--l-secondary);
--p-secondary-contrast: var(--l-dark);
--p-accent: var(--l-accent);
--p-accent-contrast: var(--l-dark);
}
html[data-theme="dark"] {
--p-fg: var(--d-light);
--p-bg: var(--d-dark);
--p-primary: var(--d-primary);
--p-primary-contrast: var(--d-dark);
--p-secondary: var(--d-secondary);
--p-secondary-contrast: var(--d-light);
--p-accent-contrast: var(--d-dark);
}
@media (prefers-color-scheme: dark) {
html[data-theme="auto"] {
--p-fg: var(--d-light);
--p-bg: var(--d-dark);
--p-primary: var(--d-primary);
--p-primary-contrast: var(--d-dark);
--p-secondary: var(--d-secondary);
--p-secondary-contrast: var(--d-light);
--p-accent-contrast: var(--d-dark);
}
}
@media (prefers-color-scheme: light) {
html[data-theme="auto"] {
--p-fg: var(--l-dark);
--p-bg: var(--l-light);
--p-primary: var(--l-primary);
--p-primary-contrast: var(--l-dark);
--p-secondary: var(--l-secondary);
--p-secondary-contrast: var(--l-dark);
--p-accent: var(--l-accent);
--p-accent-contrast: var(--l-dark);
}
}
import { defineStore } from "pinia";
import { useColorMode, useCycleList } from "@vueuse/core";
export const useThemeStore = defineStore("themeStore", () => {
const modes = {
auto: "auto",
light: "light",
dark: "dark",
};
const mode = useColorMode({
emitAuto: true,
modes,
selector: "html",
attribute: "data-theme",
});
const { state, next } = useCycleList([...Object.values(modes)], {
initialValue: mode,
});
const cookie = useCookie<{ theme: string }>("theme", {
watch: true
});
if (cookie.value)
state.value = cookie.value.theme;
function cycle() {
next();
cookie.value.theme = state.value;
state.value = cookie.value.theme;
}
watchEffect(() => {
mode.value = state.value as any;
});
return {
modes: Object.values(modes),
state,
cycle,
};
});
<script setup lang="ts">
import { useThemeStore } from "@/stores/theme";
const themeStore = useThemeStore();
</script>
<template>
<button @click="themeStore.cycle">
<Icon
v-if="themeStore.state === 'light'"
name="solar:sun-bold"
size="1.3rem"
/>
<Icon
v-if="themeStore.state === 'dark'"
name="solar:cloudy-moon-bold-duotone"
size="1.3rem"
/>
<Icon
v-if="themeStore.state === 'auto'"
name="solar:monitor-bold-duotone"
size="1.3rem"
/>
<span
class="ml-2 capitalize"
>{{ themeStore.state }}</span>
</button>
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment