Skip to content

用 Transition 做進出場動畫

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.x 指南

到目前為止,v-if 的顯示隱藏、:is 的元件切換,都是瞬間完成的「硬切」。<Transition> 讓元素的進場與退場可以套用 CSS 動畫,而且不用改任何邏輯,只要包一層。

<Transition> 基本用法

把會進出畫面的單一元素(或元件)包進 <Transition>,並給它一個 name

src/App.vue

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

const added = ref(false)
</script>

<template>
  <main class="app">
    <button type="button" @click="added = !added">
      {{ added ? '從購物車移除' : '加入購物車' }}
    </button>

    <Transition name="fade">
      <p v-if="added" class="notice">已加入購物車!</p>
    </Transition>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}

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

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

按下按鈕,提示文字淡入;再按一次,淡出後才從 DOM 移除。邏輯完全沒變,還是那個 v-if<Transition> 做的事,是在進出場的過程中幫元素掛上約定好的 CSS class,動畫本身由我們的 CSS 決定。

CSS class 的約定

<Transition name="fade"> 在進出場過程會依序掛上六個 class,名稱以 name 為前綴:

Class掛上的時機
fade-enter-from進場的起點(插入前的樣子),下一影格移除
fade-enter-active整個進場過程,transition 屬性通常寫這裡
fade-enter-to進場的終點(完成後的樣子)
fade-leave-from退場的起點
fade-leave-active整個退場過程
fade-leave-to退場的終點,動畫結束後元素才移除

所以剛才的範例翻譯過來就是:進場從 opacity: 0fade-enter-from)過渡到正常;退場從正常過渡到 opacity: 0fade-leave-to);過渡花 0.3 秒(兩個 -active)。Vue 負責在對的時機掛 class 與延後移除元素,動畫的長相全部寫在 CSS,這就是 <Transition> 的契約。

它也能包住上一節學過的東西:<Transition> 裡放 <component :is>,元件切換就有進出場;搭配 mode="out-in"(舊的先退場、新的再進場)可以避免兩個元件短暫並存的跳動。要注意 <Transition> 只接受單一子元素或元件。

<TransitionGroup>:列表的進出場

列表的新增與移除,用 <TransitionGroup>。它是 <Transition> 的列表版:允許多個子元素,每個子元素都套用同一組 class 約定:

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

const books = ref([
  { id: 1, title: '重新認識 Vue.js' },
  { id: 2, title: '008 天絕對看不完' }
])
let nextBookId = 3

function addBook() {
  books.value.push({
    id: nextBookId,
    title: `新書 ${nextBookId}`
  })
  nextBookId++
}

function removeBook(id) {
  books.value = books.value.filter(book => book.id !== id)
}
</script>

<template>
  <main class="app">
    <button type="button" @click="addBook">
      新增書籍
    </button>

    <TransitionGroup name="fade" tag="ul" class="book-list">
      <li v-for="book in books" :key="book.id" class="book-item">
        <span>{{ book.title }}</span>
        <button type="button" @click="removeBook(book.id)">
          移除
        </button>
      </li>
    </TransitionGroup>
  </main>
</template>

<style scoped>
.app {
  display: grid;
  gap: 1rem;
  padding: 2rem;
}

.book-list {
  display: grid;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.book-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

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

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
試一試
  • 重新認識 Vue.js
  • 008 天絕對看不完

tag="ul" 指定它渲染成 <ul>。這裡有一個比平常更嚴格的要求:每個子元素都必須有穩定且唯一的 key。第二章講過 key 是 Vue 追蹤列表項目的身分證;對 <TransitionGroup> 來說,沒有正確的 key,Vue 就分不清「哪一筆是新增、哪一筆是移除」,動畫會套在錯的項目上。用陣列 index 當 key 在這裡幾乎保證出錯。

本例只處理新增與移除的淡入淡出,沒有設定 fade-move。因此移除動畫結束後,其他項目會直接跳到空出的位置,這是預期行為,不是動畫失效。<TransitionGroup> 也能替「移動中」的項目掛 fade-move class,讓補位或排序有移動動畫;這屬於進階用法,需要時再查官方文件即可。

進出場動畫補完,本章的元件組合工具就全部到齊了。最後一節換一個角度收尾:把這些工具交給 AI 組裝,看看我們該在哪些地方把關。