Skip to content

VFX Transition transition

We previously used an SVG Filter implementation. Not only did it occupy the filter property on style, it was also limited by SVG support and other issues—in short, there were quite a lot of constraints. ( ˘•ω•˘ )

Thanks to snapDOM, we can easily put the DOM onto a canvas and create all sorts of cool effects! ヾ(◍'౪`◍)ノ゙

Usage examples

Basic usage

Its usage is the same as Vue’s built-in Transition component.

View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <div class="flex flex-col gap-4 border rounded">
      <base-checkbox
        v-model="visible"
        :label="$t('show')"
        class="p-4"
      />
    </div>

    <div class="flex justify-center">
      <transition-vfx>
        <div
          v-if="visible"
          class="card rounded p-6"
        >
          {{ $t('hello') }}
        </div>
      </transition-vfx>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import TransitionVfx from '../transition-vfx.vue'

const visible = ref(true)
</script>

<style scoped lang="sass">
.card
  background-color: light-dark(#edf0f2, #383e45)
</style>

<i18n lang="json">
{
  "zh-hant": {
    "show": "顯示",
    "hello": "安安"
  },
  "en": {
    "show": "Show",
    "hello": "Hello"
  }
}
</i18n>

Enter & leave

You can specify different effects for enter and leave.

Enter Effect
Leave Effect
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="grid grid-cols-6 items-center gap-2 border rounded p-4">
      <div class="col-span-2 md:col-span-1">
        {{ t('enterEffect') }}
      </div>

      <div class="col-span-4 border rounded md:col-span-2">
        <select
          v-model="enterName"
          class="w-full p-2"
        >
          <option
            v-for="option in options"
            :key="option"
            :value="option"
          >
            {{ option }}
          </option>
        </select>
      </div>

      <div class="col-span-2 md:col-span-1">
        {{ t('leaveEffect') }}
      </div>

      <div class="col-span-4 border rounded md:col-span-2">
        <select
          v-model="leaveName"
          class="w-full p-2"
        >
          <option
            v-for="option in options"
            :key="option"
            :value="option"
          >
            {{ option }}
          </option>
        </select>
      </div>

      <div
        class="col-span-6 border rounded-lg duration-300"
        :class="{
          'cursor-not-allowed opacity-30': isTransitioning,
          'cursor-pointer': !isTransitioning,
        }"
      >
        <base-checkbox
          v-model="visible"
          :label="t('show')"
          class="w-full cursor-pointer p-4"
          :class="{ 'pointer-events-none': isTransitioning }"
        />
      </div>
    </div>

    <div
      class="h-[50vh] flex items-center justify-center"
      :class="{ 'pointer-events-none': isTransitioning }"
    >
      <transition-vfx
        :enter-params="params.enter"
        :leave-params="params.leave"
        @enter="isTransitioning = true"
        @leave="isTransitioning = true"
        @after-enter="isTransitioning = false"
        @after-leave="isTransitioning = false"
      >
        <div
          v-if="visible"
          class="card flex flex-col items-center gap-4 border rounded-xl p-6"
        >
          <img
            src="/low/profile.webp"
            class="mb-4 h-[180px] w-[180px] overflow-hidden border-4 border-white rounded-full shadow-xl"
          >

          <div class="text-xl font-bold">
            {{ t('codfishName') }}
          </div>

          <base-input
            v-model="text"
            class="w-[22rem] text-center"
            :class="{
              'font-bold': styles.bold,
              'italic': styles.italic,
            }"
          />

          <div class="flex gap-2">
            <base-checkbox
              v-model="styles.bold"
              :label="t('bold')"
            />

            <base-checkbox
              v-model="styles.italic"
              :label="t('italic')"
            />
          </div>
        </div>
      </transition-vfx>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import BaseInput from '../../base-input.vue'
import TransitionVfx from '../transition-vfx.vue'
import { TransitionName } from '../type'

const { t } = useI18n()

const text = ref(t('inputPlaceholder'))
const styles = ref({
  bold: false,
  italic: false,
})

const isTransitioning = ref(false)
const visible = ref(true)
const enterName = ref<`${TransitionName}`>('shatter')
const leaveName = ref<`${TransitionName}`>('shatter')

const params = computed(() => ({
  enter: { name: enterName.value },
  leave: { name: leaveName.value },
}))

const options = Object.values(TransitionName)
</script>

<style scoped lang="sass">
.card
  background-color: light-dark(#edf0f2, #383e45)
</style>

How it works

  1. Intercept Transition events
  2. Create a canvas that covers the element and position it using CSS Anchor
  3. Use snapDOM to draw the DOM onto the canvas
  4. During enter and leave, hide the original DOM and show the canvas, like a stunt double ( •̀ ω •́ )✧
  5. Implement the enter and leave animation logic on the canvas
  6. After the animation finishes, restore the original DOM and hide the canvas

Source code

API

Props

interface Props {
  appear?: boolean;
  enterParams?: TransitionParams;
  leaveParams?: TransitionParams;
  duration?: number;
}

Emits

const emit = defineEmits<{
  (e: 'enter'): void;
  (e: 'afterEnter'): void;
  (e: 'leave'): void;
  (e: 'afterLeave'): void;
}>()

Slots

const slots = defineSlots<{
  default?: () => unknown;
}>()

v0.51.0