Crafting my Portfolio - Fix Theme switching
Tags: vue, nuxt, tailwindcss, portfolio
Published: 2022-11-07 11:58:19

> 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](https://github.com/saadeghi/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.

```zsh
npx nuxi add component NavBar/ThemeToggle
```

```vue
<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>
```

Resulting in
![Switcher](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ofs77o5rto5dhlxn76yl.png)

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, set`pastel` 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, set`pastel` 

After considering all possible edge cases and possibilities, The Initialization phase code looks something like this (thoroughly tested it)
```vue
<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.

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

```vue
<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 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zwyeg8go9hixgb28w9yi.gif)

**Test 2**

![Test 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72mb3cwxr1rg9z2cpux0.gif)

**Test 3**

![Test 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j3ue7zcm2zvbxvu2cbwj.gif)

**Test 4**

![Test 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/exgs8bro6aqjfo87uxt2.gif)

Everything's working as expected. All tests passed.

## Deployment
I already had an old design of my portfolio, which was deployed to [vercel](https://vercel.com) 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](https://hardeepkumar.in)

---

Cover Credits: [Cookie the Pom](https://unsplash.com/@cookiethepom)

---