Vue Transition 動畫

Vue 提供了 <Transition><TransitionGroup> 元件,讓你可以輕鬆地為元素或元件的進入/離開添加過渡動畫效果。

Transition 基本用法

<Transition> 用於單一元素或元件的過渡:

<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<template>
  <button @click="show = !show">切換</button>

  <Transition>
    <p v-if="show">Hello Vue!</p>
  </Transition>
</template>

<style>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}
</style>

CSS 過渡 Class

Vue 會在適當的時機自動添加/移除這些 class:

進入過渡:
v-enter-from → v-enter-active → v-enter-to

離開過渡:
v-leave-from → v-leave-active → v-leave-to
Class說明
v-enter-from進入的起始狀態
v-enter-active進入的過渡狀態(定義動畫)
v-enter-to進入的結束狀態
v-leave-from離開的起始狀態
v-leave-active離開的過渡狀態(定義動畫)
v-leave-to離開的結束狀態

命名過渡

使用 name 屬性來自訂 class 前綴:

<template>
  <Transition name="fade">
    <p v-if="show">Hello</p>
  </Transition>

  <Transition name="slide">
    <p v-if="show">World</p>
  </Transition>
</template>

<style>
/* fade 過渡 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* slide 過渡 */
.slide-enter-active,
.slide-leave-active {
  transition: transform 0.5s ease;
}

.slide-enter-from {
  transform: translateX(-100%);
}

.slide-leave-to {
  transform: translateX(100%);
}
</style>

CSS 動畫

也可以使用 CSS Animation:

<template>
  <Transition name="bounce">
    <p v-if="show">彈跳效果</p>
  </Transition>
</template>

<style>
.bounce-enter-active {
  animation: bounce-in 0.5s;
}

.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}

@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}
</style>

自訂過渡 Class

可以覆蓋預設的 class 名稱,適合搭配第三方動畫庫:

<template>
  <!-- 使用 Animate.css -->
  <Transition
    enter-active-class="animate__animated animate__fadeIn"
    leave-active-class="animate__animated animate__fadeOut"
  >
    <p v-if="show">Hello</p>
  </Transition>
</template>

可用的屬性:

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class

過渡模式

當在兩個元素間切換時,可能會有新舊元素同時存在的問題。使用 mode 來控制:

<template>
  <!-- 先離開,再進入 -->
  <Transition name="fade" mode="out-in">
    <component :is="currentComponent" />
  </Transition>

  <!-- 先進入,再離開 -->
  <Transition name="fade" mode="in-out">
    <component :is="currentComponent" />
  </Transition>
</template>

使用 key 強制過渡

<template>
  <Transition name="fade" mode="out-in">
    <p :key="count">{{ count }}</p>
  </Transition>
</template>

JavaScript Hooks

可以使用 JavaScript hooks 來控制過渡:

<template>
  <Transition
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @after-enter="onAfterEnter"
    @enter-cancelled="onEnterCancelled"
    @before-leave="onBeforeLeave"
    @leave="onLeave"
    @after-leave="onAfterLeave"
    @leave-cancelled="onLeaveCancelled"
  >
    <div v-if="show">Hello</div>
  </Transition>
</template>

<script setup>
function onBeforeEnter(el) {
  // 進入前
}

function onEnter(el, done) {
  // 進入中
  // 呼叫 done() 表示完成
  done()
}

function onAfterEnter(el) {
  // 進入後
}

function onEnterCancelled(el) {
  // 進入被取消
}

function onBeforeLeave(el) {
  // 離開前
}

function onLeave(el, done) {
  // 離開中
  done()
}

function onAfterLeave(el) {
  // 離開後
}

function onLeaveCancelled(el) {
  // 離開被取消(僅 v-show)
}
</script>

純 JavaScript 動畫

如果只使用 JavaScript 動畫,加上 :css="false" 來跳過 CSS 偵測:

<template>
  <Transition
    :css="false"
    @enter="onEnter"
    @leave="onLeave"
  >
    <div v-if="show">Hello</div>
  </Transition>
</template>

<script setup>
function onEnter(el, done) {
  // 使用 GSAP 或其他動畫庫
  gsap.to(el, {
    opacity: 1,
    duration: 0.5,
    onComplete: done
  })
}

function onLeave(el, done) {
  gsap.to(el, {
    opacity: 0,
    duration: 0.5,
    onComplete: done
  })
}
</script>

初始渲染的過渡

使用 appear 讓元素在初始渲染時也有過渡效果:

<template>
  <Transition appear name="fade">
    <p>頁面載入時會有過渡效果</p>
  </Transition>
</template>

也可以自訂初始渲染的 class:

<template>
  <Transition
    appear
    appear-from-class="custom-appear-from"
    appear-active-class="custom-appear-active"
    appear-to-class="custom-appear-to"
  >
    <p>Hello</p>
  </Transition>
</template>

TransitionGroup

<TransitionGroup> 用於列表的過渡:

<script setup>
import { ref } from 'vue'

const items = ref([1, 2, 3, 4, 5])
let id = 6

function add() {
  const index = Math.floor(Math.random() * items.value.length)
  items.value.splice(index, 0, id++)
}

function remove() {
  const index = Math.floor(Math.random() * items.value.length)
  items.value.splice(index, 1)
}
</script>

<template>
  <button @click="add">新增</button>
  <button @click="remove">移除</button>

  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item">
      {{ item }}
    </li>
  </TransitionGroup>
</template>

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

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/* 移動過渡 */
.list-move {
  transition: transform 0.5s ease;
}

/* 確保離開的元素脫離文件流 */
.list-leave-active {
  position: absolute;
}
</style>

TransitionGroup 的差異

<Transition> 的差異:

  1. 預設會渲染一個元素,使用 tag 指定
  2. 不支援 mode 屬性
  3. 每個子元素必須有唯一的 key
  4. 有額外的 v-move class 用於移動動畫

常見動畫效果

淡入淡出

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

滑動

.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s ease;
}

.slide-enter-from {
  transform: translateY(-20px);
}

.slide-leave-to {
  transform: translateY(20px);
}

縮放

.scale-enter-active,
.scale-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.scale-enter-from,
.scale-leave-to {
  transform: scale(0.9);
  opacity: 0;
}

翻轉

.flip-enter-active,
.flip-leave-active {
  transition: transform 0.5s ease;
  transform-style: preserve-3d;
}

.flip-enter-from {
  transform: rotateY(-90deg);
}

.flip-leave-to {
  transform: rotateY(90deg);
}

可重用的過渡元件

<!-- FadeTransition.vue -->
<template>
  <Transition name="fade" mode="out-in">
    <slot></slot>
  </Transition>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

使用:

<template>
  <FadeTransition>
    <div v-if="show">Hello</div>
  </FadeTransition>
</template>

搭配 Vue Router

<template>
  <router-view v-slot="{ Component }">
    <Transition name="fade" mode="out-in">
      <component :is="Component" />
    </Transition>
  </router-view>
</template>

更多路由過渡請參考 Vue Router

效能考量

  1. 使用 transformopacity:這兩個屬性可以被 GPU 加速
  2. 避免 heightwidth 動畫:這些屬性會觸發重排
  3. 使用 will-change:提示瀏覽器進行優化
.animated-element {
  will-change: transform, opacity;
}