This is a Series of Posts where I'm sharing my journey (sort of) while I craft my Portfolio.
I created the Homepage for my portfolio. (It ain't much, but it's honest work) But, I'm also noticing some bugs in theme switching, so I'll fix them as well.
Theme-o-Fix
TBH, I didn't notice that the theme swapping functionality had few strange bugs. Which are not much of a deal, but since I saw them, It's itching me to fix them. I'll try my best to explain the bugs.
The Problem
- When the device preferred theme is dark (prefers-color-scheme="dark"), then the theme would automatically switch to
dracula
but it does not affect the values in local storage, which means the toggle icons are not correctly synced. - When the theme is set to be pastel in local storage, then the icons will go out of sync.
These bugs are actually not due to theme-change package, but rather because it doesn't provide the behavior that I want. Also, showing the issue is quite not possible since It's rather way too specific.
The preferred functionality is to have sun
icon in toggle when the dracula
theme is active and moon
icon when pastel
theme is active. And also I want to track themes correctly when having prefers-color-scheme
or so I wanted to, but I don't want to do that functionality now. Instead, I want to manually manage the theme, having Dark by default and able to swap it with light if needed.
Solution
I was using theme-change to handle my theme swapping, but I think It will be better to implement it on my on.
The functionality I want to have for theme swapping now is
- Set theme as per
prefers-color
- Being able to swap between Dark and Light
- Track selected theme in local Storage
Theme switching, revisited
First, I'd like to separate the base Markup in a component to better work with it in isolation and have all the UI parts that I want to have.
npx nuxi add component NavBar/ThemeToggle
<script lang="ts" setup></script>
<template>
<div
class="btn btn-circle bg-transparent border-none loading disabled text-primary"
></div>
<div
class="btn-circle swap swap-rotate"
data-toggle-theme="pastel,dracula"
data-act-class="swap-active"
>
<!-- sun icon -->
<v-icon name="ri-sun-fill" class="swap-on fill-primary" scale="1.2" />
<!-- moon icon -->
<v-icon name="ri-moon-fill" class="swap-off fill-primary" scale="1.2" />
</div>
</template>
<style scoped></style>
The Themes are swapped using the theme-data
attribute on the HTML element, which I'll need to keep in mind while going forward. The theme swapping functionality will be divided in 3 parts:
- Initialization
- Toggling
- Monitoring
Initialization
For the Initialization part, First I want to check the prefers-color
to find the user device preference. Which will be either dark
or light
/unset.
- If dark, then check if there is any
theme
entry in LocalStorage. If there is, get its value.- If its value is
dracula
, setdracula
theme. - If its value is
pastel
, setpastel
theme. - if its value is not acceptable or not set, set
pastel
theme.
- If its value is
- If light or unset, then check if there is any
theme
entry in LocalStorage. If there is, get its value.- If its value is
dracula
, setdracula
theme. - If its value is
pastel
, setpastel
theme. - if its value is not acceptable or not set, set
pastel
- If its value is
After considering all possible edge cases and possibilities, The Initialization phase code looks something like this (thoroughly tested it)
<script lang="ts" setup>
const theme = ref('pastel');
const loading = ref(true);
function initThemeCheck(
ele: HTMLHtmlElement,
prefersModeDark: MediaQueryList,
localTheme: string
) {
if (prefersModeDark.matches) {
if (localTheme == 'dracula') {
localStorage.setItem('theme', 'dracula');
ele.setAttribute('data-theme', 'dracula');
theme.value = 'dracula';
} else if (localTheme == 'pastel') {
ele.setAttribute('data-theme', 'pastel');
theme.value = 'pastel';
} else {
localStorage.setItem('theme', 'dracula');
ele.setAttribute('data-theme', 'dracula');
theme.value = 'dracula';
}
} else {
if (localTheme == 'dracula') {
ele.setAttribute('data-theme', 'dracula');
theme.value = 'dracula';
} else if (localTheme == 'pastel') {
ele.setAttribute('data-theme', 'pastel');
theme.value = 'pastel';
} else {
localStorage.setItem('theme', 'pastel');
ele.setAttribute('data-theme', 'pastel');
theme.value = 'pastel';
}
}
}
onMounted(() => {
const ele = document.querySelector('html');
const prefersModeDark = window.matchMedia('(prefers-color-scheme: dark)');
const localTheme = localStorage.getItem('theme');
initThemeCheck(ele, prefersModeDark, localTheme);
loading.value = false;
});
</script>
Toggling
This part will take care of toggling the theme using a button like functionality, or let's say a click. On click, it will change to pastel
if dracula
and vice versa. In addition, It will also update theme
item in LocalStorage with the new selected theme. And will also change the value of data-theme
attribute on HTML element.
<script lang="ts" setup>
....
function swapTheme() {
const ele = document.querySelector('html');
if (theme.value == 'dracula') {
localStorage.setItem('theme', 'pastel');
ele.setAttribute('data-theme', 'pastel');
theme.value = 'pastel';
} else {
localStorage.setItem('theme', 'dracula');
ele.setAttribute('data-theme', 'dracula');
theme.value = 'dracula';
}
}
....
</script>
<template>
<div
class="btn btn-circle bg-transparent border-none loading disabled text-primary"
v-if="loading"
></div>
<div
class="btn-circle swap swap-rotate"
:class="{
'swap-active': theme == 'dracula',
'': theme == 'pastel',
}"
@click="swapTheme"
v-else
>
<!-- sun icon -->
<v-icon name="ri-sun-fill" class="swap-on fill-primary" scale="1.2" />
<!-- moon icon -->
<v-icon name="ri-moon-fill" class="swap-off fill-primary" scale="1.2" />
</div>
</template>
Monitoring
I also want to monitor for changes across multiple tabs, manual clearance of LocalStorage data and changing prefers-color
. For that, I'll be adding event listener on LocalStorage and prefers-color
and update theme data accordingly.
<script lang="ts" setup>
onMounted(() => {
....
prefersModeDark.addEventListener('change', () => {
if (prefersModeDark.matches) {
localStorage.setItem('theme', 'dracula');
ele.setAttribute('data-theme', 'dracula');
theme.value = 'dracula';
} else {
localStorage.setItem('theme', 'pastel');
ele.setAttribute('data-theme', 'pastel');
theme.value = 'pastel';
}
});
window.onstorage = () => {
const currentLocalTheme = localStorage.getItem('theme');
if (currentLocalTheme == 'dracula') {
localStorage.setItem('theme', 'dracula');
ele.setAttribute('data-theme', 'dracula');
theme.value = 'dracula';
} else if (currentLocalTheme == 'pastel') {
localStorage.setItem('theme', 'pastel');
ele.setAttribute('data-theme', 'pastel');
theme.value = 'pastel';
} else {
localStorage.setItem('theme', 'pastel');
ele.setAttribute('data-theme', 'pastel');
theme.value = 'pastel';
}
};
});
</script>
This takes care of all the functionality needed for theme swapping. Now time to test.
Test 1
Test 2
Test 3
Test 4
Everything's working as expected. All tests passed.
Deployment
I already had an old design of my portfolio, which was deployed to vercel and also had my custom domain mapped, all I have to do is merge the branch into main and vercel will take care of the rest. Here is the link to the deployed site https://hardeepkumar.in
Cover Credits: Cookie the Pom
Top comments (3)
You didn't want to use that module: color-mode.nuxtjs.org/ ? (Nuxt3-ready)
That article is also quite nice regarding the subject: web.dev/building-a-theme-switch-co...
Yep saw that too but it utilize classes, while daisy ui add a
data-theme
attribute to html element. So say if i want to use it, I'll still have to write a function to modify that html attribute. So i instead thought of trying to implement it myself.lovely theme