- Vue.js
Vue Transitionの使い方|フェード・スライドのコード集
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期連続の黒字決算。