QThemeButton components

Styled as a switch by default, this component is used to toggle between light- and darkmode. It recognizes the user's preferred color scheme using "(prefers-color-scheme: dark)" and saves this configuration in the browser's localstorage.

The size of this component is based on the CSS variable --el-size-xs, which describes its width. calc is used to determine all other dimensions, such as height and the size of the toggle ball.

Requirements

TypePath / VersionPurposeOptional
Vue versionVue 3Composition APINo
Styles../../assets/main.cssCSS VariablesYes
Functions../../use/uuidAssign ids to itemsNo

Usage

Import the following component/s:

import QThemeButton from '../../components/UI/Theme/QThemeButton.vue'

Basic usage

Just import and include the component into a single file.

Example

<q-theme-button />

Prefer localstorage settings

By default, the user's browser settings are preferred over those in localstorage. You can use :initWithPreference="false" to overwrite this behavior.

Example

<q-theme-button :initWithPreference="false" />

Full component's code

<template>
  <div>
    <input
      @change="toggleTheme"
      :id="themeButtonId"
      type="checkbox"
      class="q-switch-checkbox"
    />
    <label
      class="q-switch-label"
      :class="{ 'q-switch-disabled': disabled }"
      :for="themeButtonId"
    >
      <span>🌙</span>
      <span>☀️</span>
      <div
        tabindex="0"
        class="q-toggle"
        :class="{
          'q-toggle-checked': userTheme === 'dark-theme',
        }"
      ></div>
    </label>
  </div>
</template>

<script>
import uuid from "../../../use/uuid.js";
import { ref, onMounted } from "vue";

export default {
  setup({ initWithPreference, disabled }) {
    const themeButtonId = uuid();
    let userTheme = ref("");
    const toggleTheme = () => {
      if (!disabled) {
        const activeTheme = localStorage.getItem("user-theme");
        if (activeTheme === "light-theme") {
          setTheme("dark-theme");
        } else {
          setTheme("light-theme");
        }
      }
    };

    const setTheme = (theme) => {
      localStorage.setItem("user-theme", theme);
      userTheme.value = theme;
      document.documentElement.className = theme;
    };

    const getMediaPreference = () => {
      const hasDarkPreference = window.matchMedia(
        "(prefers-color-scheme: dark)"
      ).matches;
      if (hasDarkPreference) {
        return "dark-theme";
      }
    };

    onMounted(() => {
      if (initWithPreference) {
        setTheme(getMediaPreference());
      } else {
        setTheme(localStorage.getItem("user-theme"));
      }
    });

    return { themeButtonId, userTheme, toggleTheme };
  },
  props: {
    initWithPreference: {
      type: Boolean,
      default: true,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
  },
};
</script>

<style scoped>
.q-switch-checkbox {
  display: none;
}

.q-switch-label {
  align-items: center;
  background: var(--text-color-primary);
  border: calc(var(--el-size-xs) * 0.025) solid var(--black-color);
  border-radius: var(--el-size-xs);
  cursor: pointer;
  display: flex;
  font-size: calc(var(--el-size-xs) * 0.3);
  height: calc(var(--el-size-xs) * 0.5);
  position: relative;
  padding: calc(var(--el-size-xs) * 0.1);
  transition: background 0.5s ease;
  justify-content: space-between;
  width: var(--el-size-xs);
  z-index: 1;
}

.q-switch-label .q-toggle {
  position: absolute;
  background-color: var(--accent-color-primary);
  border-radius: 50%;
  top: calc(var(--el-size-xs) * 0.025);
  left: calc(var(--el-size-xs) * 0.045);
  height: calc(var(--el-size-xs) * 0.4);
  width: calc(var(--el-size-xs) * 0.4);
  transform: translateX(0);
  transition: transform 0.3s cubic-bezier(0.39, 0.58, 0.57, 1),
    background-color 0.5s ease;
  outline: none;
}

.q-switch-disabled {
  opacity: var(--opacity-disabled);
}

.q-toggle-checked {
  transform: translateX(calc(var(--el-size-xs) * 0.475)) !important;
}
</style>