skip to Main Content

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

Answers


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

    Login or Signup to reply.
  2. 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
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search