DEV Community

Cover image for Crafting my Portfolio - Fix Theme switching
Hardeep Kumar
Hardeep Kumar

Posted on

Crafting my Portfolio - Fix Theme switching

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
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

Resulting in
Switcher

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, set dracula theme.
    • If its value is pastel, set pastel theme.
    • if its value is not acceptable or not set, setpastel theme.
  • 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, set dracula theme.
    • If its value is pastel, set pastel theme.
    • if its value is not acceptable or not set, setpastel

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

This takes care of all the functionality needed for theme swapping. Now time to test.

Test 1

Test 1

Test 2

Test 2

Test 3

Test 3

Test 4

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)

Collapse
 
kissu profile image
Konstantin BIFERT

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...

Collapse
 
wrench1815 profile image
Hardeep Kumar

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.

Collapse
 
matin192hp profile image
matin HP

lovely theme