VueのTransitionとは、要素が表示されるとき・消えるときに決まった順番でCSSクラスを付け外しし、フェードやスライドなどのアニメーションを付けられる組み込みコンポーネントです。 自分で書くのは「どのクラスのときにどんな見た目にするか」というCSSだけで、クラスを付けるタイミングやアニメーション終了後の後始末は Vue が行います。

この記事では、そのまま貼って使える6種類のコード(フェード・スライド・ズーム・アコーディオン・ページ遷移・リスト並べ替え)をそろえました。すべて実際にブラウザで動かし、クラスが付く時刻やアニメーション途中の値を計測して載せています。後半では「アニメーションが効かない」ときの原因を、症状の計測結果つきで表にまとめています。

確認環境:Ubuntu 24.04 / Node.js 22.22.2 / Vue 3.5.43 / Vite 8.3.2(@vitejs/plugin-vue 6.0.9)で単一ファイルコンポーネントをビルド / vue-router 5.3.1 / Playwright 1.56.0 の Chromium 141 で実行。

Vue Transitionの早見表

やりたいこと 書き方 補足
表示・非表示にアニメーションを付ける Transition で1つの要素を包み、v-if か v-show を付ける CSS は name の値を接頭辞にしたクラスで書く
2つの要素を入れ替える v-if / v-else と key、mode="out-in" 消えてから次が現れる
同じ要素の中身が変わるたびに動かす 要素に :key を付ける key が変わると入れ替えとして扱われる
最初の表示でも動かす appear を付ける 付けないと初回は動かない
ページ遷移に付ける RouterView の v-slot で Transition を挟む mode="out-in" と :key を併用
リストの追加・削除・並べ替え TransitionGroup と v-for、各要素に :key 移動は 名前-move クラス
CSS を使わず JavaScript で動かす :css="false" とフック(@enter など) done を呼んで終了を知らせる
動きを減らす設定に合わせる @media (prefers-reduced-motion: reduce) 移動をやめて短いフェードにする

name="fade" のときに付く6つのクラスは次のとおりです。name を省くと接頭辞は v- になります。

クラス 付いている期間 書く内容
fade-enter-from 要素が挿入された瞬間から、次のフレームまで 現れる前の見た目(例:opacity: 0)
fade-enter-active 現れるアニメーションの間ずっと transition の時間とイージング
fade-enter-to 次のフレームから、アニメーション終了まで 現れた後の見た目(省略することが多い)
fade-leave-from 消え始めた瞬間から、次のフレームまで 消える前の見た目(省略することが多い)
fade-leave-active 消えるアニメーションの間ずっと transition の時間とイージング
fade-leave-to 次のフレームから、アニメーション終了まで 消えた後の見た目(例:opacity: 0)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Vue Transitionの使い方|フェード・スライドのコード集」

基本のフェードとクラスが付く順番

まず、いちばん基本のフェードです。Transition で包んだ要素に v-if を付け、CSS を4つのクラスに書きます。

<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>

<template>
  <button @click="show = !show">切り替え</button>
  <Transition name="fade">
    <p v-if="show">こんにちは</p>
  </Transition>
</template>

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

公式ドキュメントによると、Transition が動くきっかけは4つあります。v-if による条件付きレンダリング、v-show による表示切り替え、component による動的コンポーネントの切り替え、そして特別な属性 key の変更です。

クラスが付く順番を計測する

ボタンを押してからの経過時間と、そのとき要素に付いているクラスを MutationObserver で記録しました(同じ内容の行はまとめています)。

【表示するとき】
   1ms  DOMに追加   class="fade-enter-from fade-enter-active"
   9ms  クラス変更  class="fade-enter-active fade-enter-to"
 150ms  (途中経過) opacity = 0.80
 311ms  クラス変更  class=""

【消すとき】
   1ms  クラス変更  class="fade-leave-from fade-leave-active"
  14ms  クラス変更  class="fade-leave-active fade-leave-to"
 150ms  (途中経過) opacity = 0.15
 318ms  クラス変更  class=""
 318ms  DOMから削除

流れを図にすると次のようになります。

sequenceDiagram
    participant V as Vue
    participant E as 要素
    Note over V,E: 表示するとき(enter)
    V->>E: 挿入と同時に enter-from と enter-active を付ける
    V->>E: 次のフレームで enter-from を外し enter-to を付ける
    Note over E: ここで opacity が 0 から 1 へ動き出す
    V->>E: transition 終了で enter-active と enter-to を外す
    Note over V,E: 消すとき(leave)
    V->>E: leave-from と leave-active を付ける
    V->>E: 次のフレームで leave-from を外し leave-to を付ける
    Note over E: ここで opacity が 1 から 0 へ動き出す
    V->>E: transition 終了でクラスを外し、要素を取り除く

ポイントは「from で始まりの見た目を決め、1フレーム後に from を外すことで transition が動き出す」という点です。 表示するときは enter-from(opacity: 0)が外れると本来の opacity: 1 に向かって動き、消すときは leave-to(opacity: 0)が付くとそこへ向かって動きます。そのため、フェードなら enter-from と leave-to の2つに同じ内容を書くだけで済みます。

v-ifとv-showの違い

v-if を v-show に変えても同じ CSS で動きます。違いは終了後の要素の扱いです。

ディレクティブ 表示するとき(実測) 消し終わったとき(実測)
v-if 要素が DOM に追加され、enter のクラスが付く クラスが外れ、要素が DOM から削除される
v-show 既にある要素に enter のクラスが付く クラスが外れ、style に display: none が付く(要素は残る)

何度も開け閉めするものは v-show、めったに開かないものは v-if という通常の使い分けのままで構いません。

コピペ用コード集

以下のコードは、script 部分がフェードの例と同じもの(show を切り替えるだけ)については、テンプレートと CSS を中心に載せます。

スライド(横から入ってくる)

<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>

<template>
  <button @click="show = !show">切り替え</button>
  <Transition name="slide">
    <div v-if="show">右から入ってくるパネル</div>
  </Transition>
</template>

<style>
.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-enter-from,
.slide-leave-to {
  transform: translateX(40px);
  opacity: 0;
}
</style>

translateX(40px) を translateX(-40px) にすれば左から、translateY(-20px) にすれば上から入ってきます。画面の端から出てくるメニューなら translateX(100%) にします。

ズーム(拡大しながら表示)

<template>
  <button @click="show = !show">切り替え</button>
  <Transition name="zoom">
    <div v-if="show">拡大しながら表示</div>
  </Transition>
</template>

<style>
.zoom-enter-active,
.zoom-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}
.zoom-enter-from,
.zoom-leave-to {
  transform: scale(0.8);
  opacity: 0;
}
</style>

ダイアログやポップアップに向いた動きです。scale(0.8) を scale(0.95) にすると控えめになります。

3つのパターンについて、表示を始めて150ミリ秒の時点の値を計測しました。どれも途中の値になっており、アニメーションが動いていることがわかります。

パターン 150ms 時点の opacity 150ms 時点の transform クラスが外れた時刻
フェード 0.80 なし 311ms
スライド 0.74 右へ約10px の位置 310ms
ズーム 0.35 約0.87倍 368ms

公式ドキュメントは、アニメーションさせるプロパティとして transform と opacity を勧めています。レイアウトの再計算を起こさず効率がよいためです。height や margin はレイアウトの再計算を伴うので、使う場面を絞ります。

アコーディオン(高さを開閉する)

高さが決まっていない要素を開閉するときは、CSS だけでは height: auto へ向かって動かせないため、JavaScript フックで実際の高さを測って指定します。

<script setup>
import { ref } from 'vue'
const open = ref(false)

function onBeforeEnter(el) {
  el.style.height = '0'
}
function onEnter(el) {
  el.style.height = el.scrollHeight + 'px'
}
function onAfterEnter(el) {
  el.style.height = ''
}
function onBeforeLeave(el) {
  el.style.height = el.scrollHeight + 'px'
}
function onLeave(el) {
  void el.offsetHeight // 高さを一度確定させる
  el.style.height = '0'
}
function onAfterLeave(el) {
  el.style.height = ''
}
</script>

<template>
  <button @click="open = !open">詳細を{{ open ? '閉じる' : '開く' }}</button>
  <Transition
    name="acc"
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @after-enter="onAfterEnter"
    @before-leave="onBeforeLeave"
    @leave="onLeave"
    @after-leave="onAfterLeave"
  >
    <div v-show="open" class="acc-body">
      <p>1行目の説明です。</p>
      <p>2行目の説明です。</p>
      <p>3行目の説明です。</p>
    </div>
  </Transition>
</template>

<style>
.acc-body {
  overflow: hidden;
}
.acc-enter-active,
.acc-leave-active {
  transition: height 0.3s ease;
}
</style>
時点 開くとき(実測) 閉じるとき(実測)
150ms 高さ 102px 高さ 25px
終了後 高さ 127px(style.height は空に戻る) display: none

終了後に style.height を空に戻しているので、開いたあとで中身が増えても高さが追従します。overflow: hidden は、開閉の途中で中身がはみ出さないようにするために必要です。

ページ遷移(コンポーネントの切り替え)

動的コンポーネントを切り替えるときは、mode="out-in" と :key を付けます。

<script setup>
import { ref, h } from 'vue'
const Home = { render: () => h('section', 'ホーム') }
const About = { render: () => h('section', '会社概要') }
const pages = { Home, About }
const current = ref('Home')
</script>

<template>
  <button @click="current = current === 'Home' ? 'About' : 'Home'">ページ切り替え</button>
  <Transition name="page" mode="out-in">
    <component :is="pages[current]" :key="current" />
  </Transition>
</template>

<style>
.page-enter-active,
.page-leave-active {
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.page-enter-from {
  opacity: 0;
  transform: translateY(8px);
}
.page-leave-to {
  opacity: 0;
  transform: translateY(-8px);
}
</style>

Vue Router を使っている場合は、RouterView の v-slot で受け取ったコンポーネントを Transition で包みます(CSS は上と同じです)。

<script setup>
import { RouterView, RouterLink } from 'vue-router'
</script>

<template>
  <nav>
    <RouterLink to="/">ホーム</RouterLink>
    <RouterLink to="/about">会社概要</RouterLink>
  </nav>
  <RouterView v-slot="{ Component, route }">
    <Transition name="page" mode="out-in">
      <component :is="Component" :key="route.path" />
    </Transition>
  </RouterView>
</template>

vue-router 5.3.1 で「ホーム」から「会社概要」へ移動したときの記録です。

   2ms  クラス変更  ホーム   class="page-leave-from page-leave-active"
  10ms  クラス変更  ホーム   class="page-leave-active page-leave-to"
 100ms  (途中経過) 画面上の要素は「ホーム」1つ / opacity = 0.29
 210ms  DOMから削除 ホーム
 210ms  DOMに追加   会社概要 class="page-enter-from page-enter-active"
 227ms  クラス変更  会社概要 class="page-enter-active page-enter-to"
 431ms  クラス変更  会社概要 class=""

前のページが消え終わってから次のページが現れていることがわかります。ページ遷移は消える側と現れる側で合計2回分の時間がかかるので、1回を0.2秒前後の短めにしておくと軽快に感じられます。

リストの追加・削除・並べ替え(TransitionGroupとv-move)

複数の要素を扱うときは TransitionGroup を使います。公式ドキュメントでは Transition との違いとして、tag で包む要素を指定できること、mode は使えないこと、子要素には必ず一意の key が必要なこと、クラスは包む要素ではなく個々の要素に付くことを挙げています。

<script setup>
import { ref } from 'vue'
let nextId = 4
const items = ref([
  { id: 1, name: 'りんご' },
  { id: 2, name: 'みかん' },
  { id: 3, name: 'ぶどう' },
])
function add() {
  items.value.unshift({ id: nextId, name: '品目' + nextId })
  nextId++
}
function remove(id) {
  items.value = items.value.filter((i) => i.id !== id)
}
function reverse() {
  items.value.reverse()
}
</script>

<template>
  <button @click="add">先頭に追加</button>
  <button @click="reverse">逆順にする</button>
  <TransitionGroup name="list" tag="ul" class="list">
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
      <button @click="remove(item.id)">削除</button>
    </li>
  </TransitionGroup>
</template>

<style>
.list {
  position: relative;
  padding: 0;
  list-style: none;
}
.list-move,
.list-enter-active,
.list-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(20px);
}
/* 消えていく要素をレイアウトから外し、残りの要素が動けるようにする */
.list-leave-active {
  position: absolute;
}
</style>

操作ごとに、150ミリ秒時点の各要素の状態を計測しました。

操作 新しい・消える要素 ほかの要素
先頭に追加 list-enter-active が付き、opacity 0.80・右へ約4px 3つとも list-move が付き、縦方向に移動の途中
逆順にする なし 4つとも list-move が付き、それぞれ縦方向に移動の途中
2番目を削除 list-leave-active が付き、opacity 0.26・右へ約15px 下の2つに list-move が付き、上へ詰まる途中

list-move は、位置が変わる要素に Vue が自動で付けるクラスです。ここに transition を書いておくと、追加や削除で押し出される要素がなめらかに動きます。.list-leave-active の position: absolute は、消えていく要素をレイアウトの流れから外すための指定です。公式ドキュメントも、移動のアニメーションが正しく計算されるよう、この書き方を示しています。削除の計測では、消える要素がフェードしている最中に、下の要素が上へ動き始めていました。

mode・appear・keyの使い方

modeで入れ替えの順番を決める

「編集」ボタンと「保存」ボタンを v-if / v-else で入れ替える例で、3つのモードを比べました。

<script setup>
import { ref } from 'vue'
const editing = ref(false)
</script>

<template>
  <Transition name="fade" mode="out-in">
    <button v-if="editing" key="save" @click="editing = false">保存</button>
    <button v-else key="edit" @click="editing = true">編集</button>
  </Transition>
</template>
mode 150ms 時点で画面にある要素 入れ替えが終わった時刻 動き
指定なし 2つ(横に並ぶ) 約320ms 消える動きと現れる動きが同時
out-in 1つ(編集のみ) 約650ms 消え終わってから現れる
in-out 2つ(横に並ぶ) 約630ms 現れ終わってから消える

指定なしでは、一瞬2つの要素が並んで表示され、レイアウトがずれます。 同じ場所で入れ替えるなら out-in を選ぶのが定番です。

appearで最初の表示にも付ける

<template>
  <Transition name="fade" appear>
    <p>最初から表示されている要素</p>
  </Transition>
</template>
書き方 マウント直後のクラス(実測) 100ms 時点の opacity
appear なし クラスなし 1.00
appear あり fade-enter-from fade-enter-active 0.47

Transition は、最初から表示されている要素には何もしません。ページを開いたときにも動かしたい場合は appear を付けます。

keyで「中身が変わった」ことを伝える

同じタグのまま中のテキストだけが変わる場合、Vue は要素を使い回すため、Transition は動きません。:key を付けると、値が変わるたびに別の要素として入れ替えが行われます。

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <button @click="count++">+1</button>
  <Transition name="fade" mode="out-in">
    <span :key="count">{{ count }}</span>
  </Transition>
</template>
書き方 ボタンを押したあとのクラスの変化(実測)
:key なし クラスは付かず、数字だけが即座に変わる
:key="count" 0 に leave のクラス → 削除 → 1 に enter のクラス(約630ms で完了)

Vue 2のv-enterからVue 3のv-enter-fromへの変更点

Vue 3 の移行ガイドには、v-enter は v-enter-from に、v-leave は v-leave-from に名前が変わったと書かれています。あわせて、クラスを差し替える props も enter-class → enter-from-class、leave-class → leave-from-class に変わりました。

役割 Vue 2 Vue 3
現れる前の状態 .v-enter .v-enter-from
消える前の状態 .v-leave .v-leave-from
現れる前のクラスを差し替える props enter-class enter-from-class
消える前のクラスを差し替える props leave-class leave-from-class
変更なし .v-enter-active .v-enter-to .v-leave-active .v-leave-to 同じ

Vue 2 向けの記事にある CSS をそのまま Vue 3 に貼ると、どうなるかを確かめました。

.old-enter-active,
.old-leave-active {
  transition: opacity 0.3s ease;
}
/* Vue 2 の書き方:.old-enter は Vue 3 では付かない */
.old-enter,
.old-leave-to {
  opacity: 0;
}
時点 表示するとき 消すとき
付いたクラス old-enter-from old-enter-active old-leave-from old-leave-active
20ms の opacity 1.00 1.00
150ms の opacity 1.00 0.26

「消えるときはフェードするのに、現れるときは一瞬で出る」という症状になります。Vue 3 が付けるのは old-enter-from で、CSS に書いた .old-enter には当たらないためです。.old-enter を .old-enter-from に書き換えれば直ります。移行ガイドの手順も、.v-enter を .v-enter-from に、.v-leave を .v-leave-from に置き換える、というものです。

JavaScriptフックとカスタムクラス

JavaScriptフックで動かす

公式ドキュメントには8つのフックが載っています。@before-enter、@enter、@after-enter、@enter-cancelled、@before-leave、@leave、@after-leave、@leave-cancelled です。CSS を使わず JavaScript だけで動かすときは :css="false" を付け、@enter と @leave の第2引数 done を呼んで終了を知らせます。

<script setup>
import { ref } from 'vue'
const show = ref(false)

function onEnter(el, done) {
  el.animate(
    [{ opacity: 0, transform: 'translateY(-10px)' }, { opacity: 1, transform: 'none' }],
    { duration: 300, easing: 'ease-out' }
  ).finished.then(done)
}
function onLeave(el, done) {
  el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200 }).finished.then(done)
}
</script>

<template>
  <button @click="show = !show">切り替え</button>
  <Transition :css="false" @enter="onEnter" @leave="onLeave">
    <p v-if="show">通知メッセージ</p>
  </Transition>
</template>

すべてのフックにログを仕込んで、呼ばれる順番を確かめました。

【表示 → 終了を待って → 非表示】
before-enter → enter → after-enter → before-leave → leave → after-leave

【表示して100ms後に非表示(途中で切り替え)】
before-enter → enter → enter-cancelled → before-leave → leave → after-leave

途中で切り替えると after-enter の代わりに enter-cancelled が呼ばれます。終了後に行う処理(フォーカスを移すなど)は after-enter に、途中でやめたときの後始末は enter-cancelled に書き分けます。:css="false" のときは CSS のクラスが付かないことも、計測で確認できました。

カスタムクラスで@keyframesのアニメーションを使う

enter-active-class と leave-active-class で、付けるクラス名を自分で決められます。@keyframes のアニメーションや、CSS アニメーションライブラリのクラスを使うときに便利です。

<template>
  <button @click="show = !show">切り替え</button>
  <Transition enter-active-class="bounce-in" leave-active-class="bounce-out">
    <p v-if="show">お知らせ</p>
  </Transition>
</template>

<style>
.bounce-in {
  animation: bounce 0.4s;
}
.bounce-out {
  animation: bounce 0.4s reverse;
}
@keyframes bounce {
  0% { transform: scale(0); }
  60% { transform: scale(1.15); }
  100% { transform: scale(1); }
}
</style>

計測では、表示時に v-enter-from bounce-in → bounce-in v-enter-to と付き、アニメーションの長さどおり約400ミリ秒でクラスが外れました。公式ドキュメントには、transition と animation を併用するときに待つほうを指定する type や、時間を明示する duration という props も載っています。

アニメーションが効かないときの原因チェック表

「クラスは書いたのに動かない」ときは、まずブラウザの開発者ツールで、切り替えた瞬間に要素へクラスが付いているかを見ます。付いていなければ Transition の置き方、付いているのに動かなければ CSS の当たり方が原因です。

以下は、それぞれの書き方を実際に動かして確かめた結果です。

原因 クラスは付くか(実測) 症状 直し方
v-if / v-show が Transition 直下の要素ではなく、その内側の要素に付いている 付かない 即座に表示・削除される 切り替える要素を Transition の直下に置く
Transition 自体に v-if を付けている 付かない 即座に表示・削除される v-if は中の要素に付ける
子コンポーネントのルート要素が複数ある 付かない 即座に表示・削除。開発時は警告が出る 子のルートを1つの要素にまとめる
Transition の直下に要素を2つ以上置いている — コンパイルエラーになる v-if / v-else で1つだけが出るようにするか、TransitionGroup を使う
TransitionGroup の子に key がない 付かない 追加した要素が即座に現れる。開発時は警告が出る v-for の要素に一意の :key を付ける
同じタグで中身だけが変わる 付かない 中身だけが即座に変わる 要素に :key を付ける
Vue 2 の .v-enter を使っている 付く(-enter-from) 現れるときだけ動かない .v-enter-from に書き換える
scoped CSS が別コンポーネント内の要素に届いていない 付く クラスは付くが transition-duration が 0s のまま :deep() を使うか、scoped でない style に書く
最初の表示で動かない 付かない 初回だけ動かない appear を付ける

ルート要素が複数のとき

子コンポーネントのテンプレートにルート要素が2つあると、開発ビルドで次の警告が出ました。

[Vue warn] Component inside <Transition> renders non-element root node that cannot be animated.

Transition の直下に2つの要素を書いた場合は、コンパイルの時点で次のエラーになります。

<Transition> expects exactly one child element or component.

公式ドキュメントにも、Transition のスロットに入れられるのは単一の要素またはコンポーネントだけで、コンポーネントの場合はルート要素が1つである必要がある、と書かれています。

TransitionGroupのkey抜け

[Vue warn] <TransitionGroup> children must be keyed.

key がないと、Vue はどの要素が追加され、どの要素が移動したのかを判別できません。:key には配列の添字ではなく、item.id のようにデータごとに決まった値を使います。

scoped CSSで効かないとき

scoped CSS は、Transition を書いたのと同じ単一ファイルコンポーネントの中にスタイルがあれば、そのまま効きます。 包んだ要素が子コンポーネントのルートである場合や、スロットで受け取った要素を Transition で包む共通コンポーネントの場合も、scoped のまま動くことを確認しました。

効かなくなるのは、Transition が子コンポーネントの中にあり、CSS を親の scoped スタイルに書いた場合です。

<!-- BasePanel.vue(子):Transition はここにある -->
<script setup>
defineProps({ open: Boolean, name: String })
</script>

<template>
  <div class="panel-area">
    <Transition :name="name">
      <div v-if="open"><slot /></div>
    </Transition>
  </div>
</template>
<!-- 親:CSS を scoped で書いている -->
<script setup>
import { ref } from 'vue'
import BasePanel from './BasePanel.vue'
const show = ref(false)
</script>

<template>
  <div>
    <button @click="show = !show">切り替え</button>
    <BasePanel :open="show" name="pop">お知らせ</BasePanel>
  </div>
</template>

<style scoped>
.pop-enter-active,
.pop-leave-active {
  transition: opacity 0.3s ease;
}
.pop-enter-from,
.pop-leave-to {
  opacity: 0;
}
</style>
親の CSS の書き方 クラス 100ms 時点(実測)
.pop-enter-active { ... } pop-enter-from pop-enter-active が付く opacity 1.00、transition-duration 0s。消すときは約8ms で削除
:deep(.pop-enter-active) { ... } 同じように付く opacity 0.47、transition-duration 0.3s

子コンポーネントの内側の要素には親のスコープ用の属性が付かないため、親の scoped セレクタが当たりません。4つのセレクタをそれぞれ :deep() で囲むと届くようになります。アニメーション用のクラスは複数の画面で共通に使うことが多いので、scoped でない共通の CSS ファイルにまとめておく方法も扱いやすい選択です。

prefers-reduced-motionに対応する

OS の「視差効果を減らす」「アニメーションを減らす」といった設定をしている人には、大きく動くアニメーションを控えるのが親切です。CSS のメディアクエリ prefers-reduced-motion で、その設定のときだけ動きを変えられます。

<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>

<template>
  <button @click="show = !show">切り替え</button>
  <Transition name="rslide">
    <div v-if="show">パネル</div>
  </Transition>
</template>

<style>
.rslide-enter-active,
.rslide-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}
.rslide-enter-from,
.rslide-leave-to {
  transform: translateX(40px);
  opacity: 0;
}

/* 動きを減らす設定のときは、移動をやめて短いフェードだけにする */
@media (prefers-reduced-motion: reduce) {
  .rslide-enter-active,
  .rslide-leave-active {
    transition: opacity 0.1s linear;
  }
  .rslide-enter-from,
  .rslide-leave-to {
    transform: none;
  }
}
</style>

Playwright でこの設定を切り替えて計測しました。

設定 50ms 時点 クラスが外れた時刻
通常(no-preference) opacity 0.11、右へ約35px の位置 318ms
動きを減らす(reduce) opacity 0.50、移動なし 111ms

transition: none で完全に止めるより、移動や拡大をやめて短いフェードを残すほうが、表示が切り替わったことが伝わりやすくなります。Transition は CSS の transition-duration を読み取って終了を判断するので、メディアクエリで時間を変えても、クラスの後始末は正しく行われます。

よくある質問

Q. TransitionとTransitionGroupはどう使い分けますか? A. 一度に表示される要素が1つなら Transition、v-for で複数の要素を並べるなら TransitionGroup です。TransitionGroup は mode が使えない代わりに、位置が変わる要素へ 名前-move クラスを付けてくれます。

Q. 現れるときと消えるときで時間や動きを変えられますか? A. 変えられます。.fade-enter-active と .fade-leave-active を別々に書いて時間を変えたり、ページ遷移の例のように -enter-from と -leave-to に違う transform を書いたりします。

Q. アニメーションが終わったあとに処理をしたいときは? A. @after-enter と @after-leave を使います。CSS で動かしている場合でも、この2つのフックは transition の終了後に呼ばれます。今回の計測でも、0.3秒の transition で after-enter は約310ミリ秒後に呼ばれました。

Q. nameを変数にできますか? A. できます。scoped CSS の例のように :name="name" と書けば、props やデータの値で切り替えられます。「進む」と「戻る」でスライドの向きを変える、といった使い方ができます。

まとめ

  • Transition は、要素の表示・非表示に合わせて6つのクラスを決まった順番で付け外しするコンポーネントです。書くのは CSS だけで済みます。
  • 表示するときは enter-from + enter-active → 1フレーム後に enter-active + enter-to → 終了後にすべて外れる、という順番です。
  • フェード・スライド・ズームは -enter-active / -leave-active に transition、-enter-from / -leave-to に始まりと終わりの見た目を書けば動きます。
  • 入れ替えは mode="out-in"、初回表示は appear、中身の変化は :key、リストは TransitionGroup と -move クラスを使います。
  • Vue 2 の .v-enter は Vue 3 で .v-enter-from に変わりました。そのまま貼ると、現れるときだけ動きません。
  • 効かないときは、クラスが付いているかを開発者ツールで確認します。付いていなければ Transition の置き方と key、付いていれば CSS の接頭辞と scoped を見直します。
  • prefers-reduced-motion で、動きを減らす設定の人には移動をやめて短いフェードにします。

参考・出典

  • Vue.js 公式ガイド「トランジション」 https://ja.vuejs.org/guide/built-ins/transition
  • Vue.js 公式ガイド「TransitionGroup」 https://ja.vuejs.org/guide/built-ins/transition-group
  • Vue 3 移行ガイド「Transition Class Change」 https://v3-migration.vuejs.org/breaking-changes/transition.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Vue Transitionの使い方|フェード・スライドのコード集」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?