【Vue.js】transitionコンポーネントを使ってアニメーションを実装する方法

【Vue.js】transitionコンポーネントを使ってアニメーションを実装する方法 Vue.js

Vueの<Transition>は、要素の表示・非表示にアニメーションを付けるためのコンポーネントです。モーダル、通知、メッセージの出し入れなど、状態が切り替わる場面で使います。この記事では、Vue 3(<script setup>)を前提に、CSSによる基本から、JavaScriptフック、リストのアニメーションまでを動くコードで整理します。

スポンサーリンク

基本の使い方

アニメーションさせたい要素を<Transition>で囲み、nameを付けます。CSSには、nameに応じたクラスを書きます。v-ifで要素が挿入・削除されるタイミングで、Vueがクラスを自動で付け外しします。

Vue:フェードイン・フェードアウト
<script setup>
import { ref } from 'vue'

const show = ref(false)
</script>

<template>
  <button @click="show = !show">Toggle</button>

  <Transition name="fade">
    <p v-if="show">Hello, Vue.js!</p>
  </Transition>
</template>

<style>
/* 表示・非表示の両方で使う「動き方」の指定 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

/* 表示の開始状態と、非表示の終了状態 */
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

6つのCSSクラス

Vue 3では、表示時と非表示時に、それぞれ3つ、合わせて6つのクラスが付きます。クラス名はnameに応じて変わり、nameがなければv-から始まります。

CSS:Transitionが付け外しするクラス
/* Transition が自動で付け外しする6つのクラス(name="fade" の場合) */

.fade-enter-from   /* 表示の開始状態。要素が挿入される直前に付き、次のフレームで外れる */
.fade-enter-active /* 表示中ずっと付く。transition の長さ・イージングをここに書く */
.fade-enter-to     /* 表示の終了状態。開始の次のフレームで付く */

.fade-leave-from   /* 非表示の開始状態。離脱が始まった直後に付く */
.fade-leave-active /* 非表示中ずっと付く */
.fade-leave-to     /* 非表示の終了状態 */

Vue 2では、表示の開始状態が.fade-enterでした。Vue 3では.fade-enter-fromに変わっています。古いクラス名のままだと、表示時のアニメーションが効かないので注意してください。

Transitionが動く条件と制約

Transitionが動くのは、次のような切り替えのときです。

  • v-if、v-elseによる表示・非表示
  • v-showによる表示の切り替え
  • <component :is="…">による動的コンポーネントの切り替え
  • key属性の変更

制約として、<Transition>の中に入れられるのは1つの要素、またはルートが1つのコンポーネントだけです。複数の要素を入れたいときは、divなどで1つにまとめます。v-ifとv-showの違いはv-ifで条件分岐させる方法をご覧ください。

要素を入れ替える:mode

2つの要素を切り替えるとき、既定では、古い要素の退場と新しい要素の入場が同時に始まります。mode="out-in"を付けると、古い要素が消えてから、新しい要素が表示されます。同じタグの要素を入れ替える場合は、keyを付けて区別します。

Vue:mode=”out-in”
<script setup>
import { ref } from 'vue'

const saved = ref(true)
</script>

<template>
  <button @click="saved = !saved">切り替え</button>

  <!-- out-in:古い要素が消えてから、新しい要素を表示する -->
  <Transition name="fade" mode="out-in">
    <button v-if="saved" key="edit">Edit</button>
    <button v-else key="save">Save</button>
  </Transition>
</template>

初期表示でも動かす:appear、時間を指定する:duration

Vue:appearとduration
<!-- 初回表示のときにも、トランジションを動かす -->
<Transition name="fade" appear>
  <p>ページを開いたときにフェードイン</p>
</Transition>

<!-- 時間を明示する(CSS から自動検出させたくない場合) -->
<Transition name="fade" :duration="550">...</Transition>
<Transition name="fade" :duration="{ enter: 500, leave: 800 }">...</Transition>

appearを付けると、初回の描画時にもトランジションが動きます。durationは、Vueが終了を判断する時間をミリ秒で明示するものです。transitionとanimationを同じ要素で使う場合は、type属性でどちらを基準にするかを指定します。

animate.cssなどのライブラリと組み合わせる

クラス名を指定するpropsを使うと、animate.cssのようなCSSライブラリを、そのまま使えます。指定できるのは、enter-from-class、enter-active-class、enter-to-classと、それぞれに対応するleave-のpropsです。

Vue:animate.cssのクラスを指定する
<template>
  <button @click="show = !show">Toggle</button>

  <!-- animate.css のクラス名を、そのまま指定する -->
  <Transition
    enter-active-class="animate__animated animate__bounceIn"
    leave-active-class="animate__animated animate__bounceOut"
  >
    <p v-if="show">Hello, Vue.js!</p>
  </Transition>
</template>

animate.cssは、別途CSSファイルを読み込む必要があります。クラス名にはanimate__のプレフィックスが付きます。

JavaScriptフックで細かく制御する

CSSでは表現しにくい動きは、フックでJavaScriptから制御できます。:css="false"を付けると、VueはCSSの検出をやめ、JavaScriptだけで制御します。フックは@before-enter、@enter、@after-enter、@before-leave、@leave、@after-leaveなどです。

Vue:JavaScriptフックでopacityを補間する
<script setup>
import { ref } from 'vue'

const show = ref(false)

// 0〜1 を duration の間に補間して、毎フレーム opacity に反映する
function animateOpacity(el, from, to, duration, done) {
  const start = performance.now()
  function step(now) {
    const t = Math.min((now - start) / duration, 1)
    el.style.opacity = from + (to - from) * t
    if (t < 1) {
      requestAnimationFrame(step)
    } else {
      done() // 終わったことを Vue に知らせる
    }
  }
  requestAnimationFrame(step)
}

function onBeforeEnter(el) {
  el.style.opacity = 0
}
function onEnter(el, done) {
  animateOpacity(el, 0, 1, 500, done)
}
function onLeave(el, done) {
  animateOpacity(el, 1, 0, 500, done)
}
</script>

<template>
  <button @click="show = !show">Toggle</button>

  <!-- :css="false" で、CSS の検出をやめて JS だけで制御する -->
  <Transition
    :css="false"
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @leave="onLeave"
  >
    <p v-if="show">Hello, Vue.js!</p>
  </Transition>
</template>

@enterと@leaveでは、第2引数のdoneを、動きが終わったときに呼びます。doneを受け取るフックでは、Vueはdone()が呼ばれるまで完了を待つため、呼び忘れると、離脱した要素の削除などが進みません。単純なsetTimeoutで値を切り替えるだけでは、アニメーションにはならないので、毎フレームの補間か、GSAPなどのライブラリを使います。

リストのアニメーション:TransitionGroup

リストの項目の追加・削除・並べ替えには、<TransitionGroup>を使います。<Transition>との違いは次の4点です。

  • tag属性を指定すると、その要素でラップされる(既定ではラップしない)
  • modeは使えない
  • 各要素に、一意のkeyが必須
  • CSSクラスは、コンテナではなく、個々の要素に付く
Vue:TransitionGroupでリストを動かす
<script setup>
import { ref } from 'vue'

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

function add() {
  items.value.splice(Math.floor(Math.random() * items.value.length), 0, next++)
}
function remove(n) {
  items.value = items.value.filter(i => i !== n)
}
</script>

<template>
  <button @click="add">追加</button>

  <!-- tag を指定すると、その要素でラップされる。key は必須 -->
  <TransitionGroup name="list" tag="ul">
    <li v-for="n in items" :key="n" @click="remove(n)">{{ n }}</li>
  </TransitionGroup>
</template>

<style>
/* move は、他の項目の追加・削除で位置が動くときのアニメーション */
.list-move,
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}

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

/* 離脱中の項目をレイアウトから外し、残りの項目がなめらかに詰まるようにする */
.list-leave-active {
  position: absolute;
}
</style>

.list-moveは、ほかの項目の追加・削除で位置がずれる項目に付き、なめらかに動かします。ただし、離脱中の項目が場所を占有していると、残りの項目の移動が正しく計算されません。.list-leave-activeにposition: absoluteを指定するのは、そのためです。keyにインデックスを使ってはいけない理由はv-forで配列やオブジェクトをループして表示する方法で解説しています。

パフォーマンスとアクセシビリティ

アニメーションには、transformとopacityを使うのがおすすめです。レイアウトの再計算が不要で、GPUでの描画が使えます。heightやmarginのアニメーションは、負荷が高いので避けます。

OSで「動きを減らす」設定をしているユーザーには、動きを抑える配慮もしておくと親切です。

CSS:動きを減らす設定への配慮
/* 動きを減らす設定のユーザーには、アニメーションを抑える */
@media (prefers-reduced-motion: reduce) {
  .fade-enter-active,
  .fade-leave-active {
    transition: none;
  }
}

応用例

よくある質問(FAQ)

Qフェードインが効かず、いきなり表示されるのはなぜですか?
AVue 2のクラス名.fade-enterを使っている可能性があります。Vue 3では.fade-enter-fromです。
QTransitionの中に複数の要素を入れられますか?
A入れられません。1つの要素か、ルートが1つのコンポーネントだけです。複数あるときは1つの要素でまとめるか、リストなら<TransitionGroup>を使ってください。
Q要素の入れ替えのとき、2つが同時に見えてしまいます。
Amode="out-in"を付けてください。古い要素が消えてから、新しい要素が表示されます。
QJavaScriptフックで、done()を呼ばないとどうなりますか?
AVueは完了を待ち続けるので、離脱した要素がDOMから取り除かれません。フックの第2引数doneは、動きが終わったときに必ず呼んでください。

まとめ

<Transition>は、nameに応じた6つのクラスで、表示・非表示のアニメーションをCSSだけで書けます。Vue 3では-enter-fromなどのクラス名に注意してください。切り替えにはmode、リストには<TransitionGroup>、細かい制御にはJavaScriptフックを使い、transformとopacityを中心にすると、軽く動かせます。