How do you do a theme change animation in vuetify?

83 Views Asked by At

Steps to see what I mean:

  1. Head to the vuetify website
  2. Click on the cog icon
  3. Select either dark or light theme.

As you alternate between different themes, an animation plays. A circle (starting from roughly the position of the button) expands, eventually engulfing the entire screen in the new theme.

I can't find any instructions in the docs on how that can be replicated. For me, a transition plays where the colours fade into their new variants, but it looks quite tacky compared to what they have going on.

Is this a feature of vuetify? How can I replicate this behaviour?

2

There are 2 best solutions below

3
Kael Watts-Deuchar On BEST ANSWER

The one on vuetifyjs.com works by copying the entire DOM, setting the correct scroll positions on the copied elements, then applying a css clip-path transition to reveal the original DOM with the new theme applied.

https://github.com/vuetifyjs/vuetify/blob/60218ee82cabaf0d6c1f22d64a24c0e32b4e4247/packages/docs/src/App.vue#L96

watch(theme.global.name, themeTransition)

function themeTransition () {
  const x = performance.now()
  for (let i = 0; i++ < 1e7; i << 9 & 9 % 9 * 9 + 9);
  if (performance.now() - x > 10) return

  const el = document.querySelector('[data-v-app]')
  const children = el.querySelectorAll('*')

  children.forEach(el => {
    if (hasScrollbar(el)) {
      el.dataset.scrollX = String(el.scrollLeft)
      el.dataset.scrollY = String(el.scrollTop)
    }
  })

  const copy = el.cloneNode(true)
  copy.classList.add('app-copy')
  const rect = el.getBoundingClientRect()
  copy.style.top = rect.top + 'px'
  copy.style.left = rect.left + 'px'
  copy.style.width = rect.width + 'px'
  copy.style.height = rect.height + 'px'

  const targetEl = document.activeElement
  const targetRect = targetEl.getBoundingClientRect()
  const left = targetRect.left + targetRect.width / 2 + window.scrollX
  const top = targetRect.top + targetRect.height / 2 + window.scrollY
  el.style.setProperty('--clip-pos', `${left}px ${top}px`)
  el.style.removeProperty('--clip-size')

  nextTick(() => {
    el.classList.add('app-transition')
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        el.style.setProperty('--clip-size', Math.hypot(window.innerWidth, window.innerHeight) + 'px')
      })
    })
  })

  document.body.append(copy)

  copy.querySelectorAll('[data-scroll-x], [data-scroll-y]').forEach(el => {
    el.scrollLeft = +el.dataset.scrollX
    el.scrollTop = +el.dataset.scrollY
  })

  function onTransitionend (e) {
    if (e.target === e.currentTarget) {
      copy.remove()
      el.removeEventListener('transitionend', onTransitionend)
      el.removeEventListener('transitioncancel', onTransitionend)
      el.classList.remove('app-transition')
      el.style.removeProperty('--clip-size')
      el.style.removeProperty('--clip-pos')
    }
  }
  el.addEventListener('transitionend', onTransitionend)
  el.addEventListener('transitioncancel', onTransitionend)
}

function hasScrollbar (el) {
  if (!el || el.nodeType !== Node.ELEMENT_NODE) return false

  const style = window.getComputedStyle(el)
  return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight)
}
.app-copy
  position: fixed !important
  z-index: -1 !important
  pointer-events: none !important
  contain: size style !important
  overflow: clip !important

.app-transition
  --clip-size: 0
  --clip-pos: 0 0
  clip-path: circle(var(--clip-size) at var(--clip-pos))
  transition: clip-path .35s ease-out
1
Mohesn Mahmoudi On

Here is a simple example of how to achieve this:

<template>
  <v-app :dark="darkTheme">
    <v-btn @click="toggleTheme">Change Theme</v-btn>
    <v-main>
      <transition name="circle-expand">
        <div :class="themeClass" v-if="isThemeVisible">
          <!-- Your application content here -->
        </div>
      </transition>
    </v-main>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      darkTheme: false,
      isThemeVisible: true
    };
  },
  computed: {
    themeClass() {
      return {
        'theme--light': !this.darkTheme,
        'theme--dark': this.darkTheme
      };
    }
  },
  methods: {
    toggleTheme() {
      this.isThemeVisible = false; // Hide the content during transition
      setTimeout(() => {
        this.darkTheme = !this.darkTheme; // Toggle theme
        this.isThemeVisible = true; // Show the content after theme change
      }, 500); // Adjust the delay to match the transition duration
    }
  }
};
</script>

<style>
.circle-expand-enter-active,
.circle-expand-leave-active {
  transition: all 0.5s ease;
}

.circle-expand-enter,
.circle-expand-leave-to {
  opacity: 0;
}

.circle-expand-enter {
  transform: scale(0);
}

.circle-expand-leave-to {
  transform: scale(2);
}
</style>

Clicking the button will toggle the darkTheme boolean, which applies the dark or light theme to the entire Vuetify application. The transition animation remains the same, smoothly transitioning the theme change with the expanding circle effect. Adjust the transition duration and timing functions as needed.