Vue 3の <Transition> は、要素がDOMに入るときと出るときにCSSクラスを順番に付け外しする組み込みコンポーネントです。動き自体はCSSの transition や animation が担い、Vueは「いつどのクラスを付けるか」を管理します。リストの追加・削除・並べ替えには <TransitionGroup> を使います。
この記事は2026年11月8日時点の安定版Vue 3.5.45を前提にしています。挙動はVue 3.5.45の開発用ビルドとヘッドレスのChrome 156で実行して確かめ、コード例は @vue/compiler-sfc でコンパイルが通ることを確認しました。
まとめ:Vue 3のTransitionの要点
- 反応するのは
v-if・v-show・<component :is>・keyの変更の4つ。中に置ける要素は1つだけ。 - クラスは
v-enter-from・v-enter-active・v-enter-toとleave側の3つで計6つ。name="fade"で接頭辞がfade-に変わる。 appearの初回表示に付くのはenterのクラス。v-appear-fromはappear-from-classで指定したときだけ使われる。- 入れ替えは
mode="out-in"が基本。既定では新旧の要素が同時にDOMに並ぶ。 - リストは
<TransitionGroup>。keyに配列の番号を使わず、削除する要素にはposition: absoluteと幅の指定を付け、親をposition: relativeにする。 - ページ遷移は
<router-view>のスロットで包み、同じ部品を使い回すルートには:key="route.path"を付ける。<KeepAlive>は<Transition>の内側に置く。 - 3.5.43以前は、複数ルートの部品を
out-inで切り替えると画面が空になる。3.5.45へ更新し、ページ部品のルート要素は1つにする。
以下、受け持つ範囲から順に説明します。
Vue 3のTransitionが受け持つ範囲とCSSだけで済む動きの境界
Transitionが反応する4つの切り替え
Vue公式ガイドのトランジションの章が挙げる発火条件は、v-if、v-show、<component> による動的コンポーネントの切り替え、特殊属性 key の変更の4つです。組み込みコンポーネントなので、インポートや登録は要りません。
DOMに入りも出もしない要素の色やサイズの変化には反応しません。ホバーで色を変えるといった動きは、:class の付け替えか :style のバインドで書きます。
CSSのtransition・animationとVueのTransitionの役割分担
「トランジション」は、ある状態から別の状態へ移るときの変化を指す言葉です。CSSの transition は2つの状態の間を補間し、animation は @keyframes で途中の状態まで定義します。
Vueの <Transition> はどちらとも競合しません。v-if で消す要素は、削除を待たせないとアニメーションの前にDOMから外れてしまいます。Vueはクラスを付けて動きを始めさせ、transitionend か animationend を待ってから要素を外します。
Transitionを使わずCSSだけで書ける条件
@starting-style と transition-behavior: allow-discrete を使えば、Vue側の仕組み無しでも一部の動きを書けます。
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<button @click="open = !open">切り替え</button>
<p class="panel" v-show="open">パネル</p>
</template>
<style>
.panel {
opacity: 1;
transition: opacity 0.3s linear, display 0.3s allow-discrete;
}
@starting-style {
.panel { opacity: 0; }
}
.panel[style*="display: none"] {
opacity: 0;
}
</style>
Chrome 156での結果は次のとおりでした。
| 書き方 | 現れる動き | 消える動き |
|---|---|---|
v-show+CSSのみ |
動く | 動く |
v-if+CSSのみ |
動く | 動かない(即時に削除) |
<Transition> |
動く | 動く |
v-if の消える動きはCSSでは書けません。さらに MDNの transition-behavior はBaseline未達で、互換データではFirefoxは display の遷移に未対応、Safariは18からの対応です。@starting-style 自体は2024年8月から主要ブラウザで使えます。製品では、現れる動きだけならCSSのみで済ませ、消える動きが要る箇所は <Transition> を使うのが安全です。
Vue 3のTransitionの基本の書き方
フェードイン・フェードアウトの最小構成です。<script setup> の書き方はComposition APIの書き方とOptions APIとの違いで説明しています。
<script setup>
import { ref } from 'vue'
const show = ref(true)
</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>
時間とイージングは -active クラスに、開始と終了の値は -enter-from と -leave-to に書きます。動かすプロパティは opacity と transform に寄せてください。height や margin はフレームごとにレイアウト計算が走るため、公式ガイドも注意を促しています。
v-ifとv-showの違いと選び方
終わった後のDOMが違います。v-show は消える動きの後も要素が残り、style="display: none;" が付くだけでした。v-if は要素ごと取り除きます。頻繁に開閉するドロップダウンには v-show が向いています。閉じたときに子コンポーネントの状態を破棄したいモーダルは、そのコンポーネントに v-if を付けます。親のrefやストアに保存した状態は残ります。@leave-cancelled が呼ばれるのは v-show のときだけです。
6つのトランジションクラスが付く順番
enterとleaveにそれぞれ3つ、計6つのクラスが付きます。
| クラス | 付くとき | 外れるとき |
|---|---|---|
v-enter-from |
挿入の直前 | 挿入の1フレーム後 |
v-enter-active |
挿入の直前 | enter終了時 |
v-enter-to |
挿入の1フレーム後 | enter終了時 |
v-leave-from |
leave開始直後 | 1フレーム後 |
v-leave-active |
leave開始直後 | leave終了時 |
v-leave-to |
leave開始の1フレーム後 | leave終了時 |
Chrome 156で記録した順番も、この表のとおりでした。公式ガイドには、CSS animationでは *-enter-from が animationend まで残るとあります。しかし3.5.45では、animationでもtransitionと同じく挿入の次のフレームで外れました。@keyframes を使う場合、-enter-from の値に頼らず -enter-active に動きを全部書くのが確実です。
name属性で変わるクラスの接頭辞
name を付けないとクラスは v- で始まり、name="fade" なら fade-enter-from になります。:name="transitionName" のように動的にもでき、状態に応じてスライドの向きを変えられます。
カスタムクラスでTailwind CSSやAnimate.cssと組み合わせる方法
enter-active-class などの6つのpropsで、クラス名そのものを差し替えられます。Tailwind CSS 4.3.5で確かめた書き方です。先の基本例のscriptと切り替えボタンを残し、Transition部分を次のコードに置き換えます。
<template>
<Transition
enter-active-class="transition-opacity duration-300 motion-reduce:transition-none motion-reduce:duration-0"
enter-from-class="opacity-0"
leave-active-class="transition-opacity duration-300 motion-reduce:transition-none motion-reduce:duration-0"
leave-to-class="opacity-0"
>
<p v-if="show">Tailwindで動かす</p>
</Transition>
</template>
動きを抑える設定では motion-reduce:transition-none だけでは足りません。transition-property が none になっても duration-300 が残り、要素は動かないまま約0.3秒残りました。motion-reduce:duration-0 を足すと、すぐに外れます。Animate.cssも enter-active-class="animate__animated animate__tada" のように渡せます。transitionとanimationを併用する場合は、どちらの終了を待つかを type で明示します。
appearで初回表示を動かすときのクラス名
ページを開いた時点で表示されている要素は、既定では動きません。初回表示から動かすには appear を付けます。
<template>
<!-- 初回表示は fade-enter-from / fade-enter-active が付く -->
<Transition name="fade" appear>
<p>初回から動く</p>
</Transition>
<!-- 初回表示だけ別の動きにしたいときはクラス名をpropsで指定する -->
<Transition
name="fade"
appear
appear-from-class="fade-appear-from"
appear-active-class="fade-appear-active"
>
<p>初回だけ別の動き</p>
</Transition>
</template>
<style>
.fade-enter-active {
transition: opacity 0.3s ease;
}
.fade-enter-from {
opacity: 0;
}
.fade-appear-active {
transition: transform 0.6s ease, opacity 0.6s ease;
}
.fade-appear-from {
opacity: 0;
transform: translateY(20px);
}
</style>
fade-appear-from や v-appear-from というクラスは、Vue 3では自動では付きません。Transition.tsで appearFromClass = enterFromClass が既定値になっており、初回表示の要素に付いたのは fade-enter-from fade-enter-active でした。CSSに .fade-appear-from を書いたのに効かないときは、propsを渡していないことが原因です。
modeによる要素の入れ替え順の制御
v-if/v-else や動的コンポーネントで要素を入れ替えるときは、mode で順番を決めます。
<script setup>
import { ref } from 'vue'
const saved = ref(true)
</script>
<template>
<Transition name="fade" mode="out-in">
<button v-if="saved" key="edit" @click="saved = false">編集</button>
<button v-else key="save" @click="saved = true">保存</button>
</Transition>
</template>
| mode | 順番 | DOMに同時に並ぶ数 | 向く場面 |
|---|---|---|---|
| 指定なし | leaveとenterが同時 | 2 | 重ねて表示するUI |
out-in |
leave完了後にenter | 1 | ボタン・タブ・ページ |
in-out |
enter完了後にleave | 2 | 新要素の表示まで旧要素を残すUI |
指定なしでは両方がレイアウトに参加するため、位置がずれます。入れ替えは out-in を既定にしてください。連続で40ミリ秒おきに5回切り替えても、out-in ではDOMに並ぶ要素は最大1つで、最後の値が残りました。
JavaScriptフックの順番には癖があります。out-in でも、新要素の @before-enter は旧要素の @after-leave より先に呼ばれました。旧要素の片付けを @after-leave に書き、新要素がその結果を読む設計は順番が逆になります。
中身だけ変わる要素は、key を変えて別要素として扱わせます。<span :key="count">{{ count }}</span> は値が変わるたびに @enter が呼ばれ、key が無いと1回も呼ばれませんでした。
TransitionGroupによるリストの追加・削除・並べ替え
TransitionGroupの基本構文と一意なkeyの指定
<script setup>
import { ref } from 'vue'
const items = ref([1, 2, 3])
let next = 4
const add = () => items.value.splice(Math.floor(Math.random() * items.value.length), 0, next++)
const remove = (i) => items.value.splice(i, 1)
</script>
<template>
<button @click="add">追加</button>
<TransitionGroup name="list" tag="ul" class="list">
<li v-for="(item, i) in items" :key="item" @click="remove(i)">{{ item }}</li>
</TransitionGroup>
</template>
<style>
.list {
position: relative;
padding: 0;
}
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.4s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-leave-active {
position: absolute;
width: 100%;
}
</style>
公式ガイドが挙げる <Transition> との違いは4つです。各要素に一意の key が必須、クラスは各要素に付く、mode は使えない、ラッパーは tag を指定したときだけ描画される。tag を省くと、<ul> の中に置いても余分な要素は増えません。HTMLに直接書くテンプレートでは <transition-group> とケバブケースで書きます。
v-moveと削除時のposition: absoluteで崩れる幅
.list-move にtransitionを書くと、位置が変わった要素が滑らかに動きます。クラス名は name に -move を付けたもので、move-class で変えられます。
公式の例にある .list-leave-active { position: absolute; } の役割は、実測すると明確でした。これが無いと、消える要素が場所を占めたままなので、残りの要素は削除完了まで移動せず、実測では消えた瞬間に項目の高さに相当する20ピクセル分跳びました。-move クラスも付きません。
ただしこの1行だけでは別の崩れが起きます。幅300ピクセル・paddingとmarginが0の <ul> に数字1文字の項目を置き、ビューポート幅1280ピクセルで確かめた結果です。8pxは文字やフォント、1280pxはビューポート幅に依存します。
| CSS | 消える要素の幅 |
|---|---|
position: absolute のみ |
8px(中身の幅まで縮む) |
width: 100% を追加 |
1280px(画面幅に広がる) |
さらに親へ position: relative |
300px(元のまま) |
背景色や枠線のあるカードでは、この縮みがそのまま見えます。上のコード例のように、absolute・width: 100%・親の position: relative をセットで書いてください。親に padding があると絶対配置の幅はその分広がるので、例では0にしています。要素を少しずつ遅らせる演出は、:data-index="index" を持たせてJavaScriptフックで遅延を計算するのが公式の書き方です。
TransitionGroupが動かない2つの原因
| 原因 | 実測の結果 | 直し方 |
|---|---|---|
:key="index" |
先頭を消すと末尾が消える動きになる | データのIDをkeyにする |
子が display: inline |
moveクラスは付くが動かない | inline-block にする |
どちらも警告は出ません。特に :key="index" は、A・B・C の先頭を消したときに A ではなく末尾の C の要素が消える動きをしました。番号が振り直されて、Vueが「末尾が消えた」と判断するためです。<span> のタグ一覧を並べ替えるときは display も確認してください。transform はインライン要素には効きません。
Vue RouterとNuxtのページ遷移にTransitionを付ける手順
router-viewのkeyとKeepAliveの入れ子順
Vue Routerでは <router-view> のスロットで受け取った部品を包みます。公式のトランジションの章とスロットの章の形を合わせると次のようになります。
<template>
<router-view v-slot="{ Component, route }">
<Transition :name="route.meta.transition || 'fade'" mode="out-in">
<KeepAlive>
<component :is="Component" :key="route.path" />
</KeepAlive>
</Transition>
</router-view>
</template>
Vue 3.5.45とVue Router 5.4.0で確かめると、落とし穴は2つありました。/users/1 から /users/2 のように同じ部品を使い回すルートは、key が無いと @enter が1回も呼ばれません。入れ子の順を <KeepAlive> が外側になるよう逆にすると、警告なしで状態が失われ、戻ったときにカウンターが0に戻りました。正しい順では値が残ります。
初回表示の扱いにも注意が必要です。公式によれば、ナビゲーションが非同期なので、appear を付けなくても最初のページで動きが走ります。止めたいときは await router.isReady() の後に app.mount('#app') を呼びます。
Nuxt 4のpageTransitionとlayoutTransition
Nuxt 4.6.4では、ページ全体の遷移を設定ファイルで有効にします。Nuxt 4の現行版と導入手順も参照してください。
export default defineNuxtConfig({
app: {
pageTransition: { name: 'page', mode: 'out-in' },
layoutTransition: { name: 'layout', mode: 'out-in' },
},
})
CSSは .page-enter-active のように、name に合わせて書きます。ページごとに変えるなら definePageMeta の pageTransition です。Nuxt公式は、レイアウトも変わる遷移ではページのトランジションは動かず、レイアウト側の設定が要ると書いています。ブラウザ標準のView Transitions APIは experimental.viewTransition で有効にできますが、名前のとおり実験的な機能です。
out-inで画面が空になる条件と修正された版
ページ部品のルートが1要素でないと、out-in の切り替えで画面が空になることがありました。版ごとの実測結果です。
| 切り替える部品 | 3.5.43 | 3.5.44 | 3.5.45 | 3.6.0-rc.14 |
|---|---|---|---|---|
| ルートが複数要素 | 空になる | 正常 | 正常 | 正常 |
| コメント+要素 | 空になる | 正常 | 正常 | 正常 |
| テキストのみ | 空+エラー | 空+エラー | 正常 | 空+エラー |
@leave で即 done() |
空になる | 空になる | 正常 | 空になる |
修正は3.5.44と3.5.45の変更履歴に #15853 と #11824 として載っています。3.5.43以前を使っているなら更新してください。更新後も、複数ルートの部品は切り替えられるだけでアニメーションせず、開発時に renders non-element root node の警告が出ます。Nuxt公式もページのルートは1要素と定めています。部品を <div> で包むのが確実な対処です。
JavaScriptフックによるGSAP連携と終了タイミングの制御
CSSで書きにくい動きは、フックで外部ライブラリに任せます。フックは @before-enter・@enter・@after-enter・@enter-cancelled と、leave側の同じ4つがenter/leave用です。初回表示には、これらとは別にappear系のフックもあります。
<script setup>
import { ref } from 'vue'
import gsap from 'gsap'
const show = ref(true)
function onBeforeEnter(el) {
el.style.opacity = 0
}
function onEnter(el, done) {
gsap.to(el, { opacity: 1, duration: 0.4, onComplete: done })
}
function onLeave(el, done) {
gsap.to(el, { opacity: 0, duration: 0.4, onComplete: done })
}
</script>
<template>
<button @click="show = !show">切り替え</button>
<Transition
:css="false"
@before-enter="onBeforeEnter"
@enter="onEnter"
@enter-cancelled="el => gsap.killTweensOf(el)"
@leave="onLeave"
>
<div v-if="show">GSAPで動かす</div>
</Transition>
</template>
:css="false" を付けると、CSSトランジションの自動検出を省き、Vueによるトランジションクラスの付与も行わなくなります。このとき @enter と @leave では第2引数の done を必ず呼びます。done を受け取らない1引数の関数にすると、Vueはその場で終了とみなし、@after-enter が同じタイミングで呼ばれました。ライブラリ側の書き方はAnime.js v4の変更点と使い方でGSAPと比べています。
入れ子の要素を時間差で動かしても、自動の終了判定は直接の子要素だけを見ます。親が0.2秒、内側が0.25秒遅れの0.3秒のとき、内側の動きが終わる前に要素が外れました。内側の遅延を含む合計を :duration="550" や :duration="{ enter: 500, leave: 800 }" のようにミリ秒で渡します。
Vue 3のTransitionが動かないときの確認点
原因はおおむね決まっています。上から順に確かめてください。
nameとCSSクラスの不一致で要素が即座に消える症状
name とCSSのクラス名が食い違っていても、エラーは出ません。対応する transition が見つからない要素は、数フレームでDOMから外れます。動かないのに表示の切り替えだけは起きるときは、開発者ツールで -enter-active のクラス名を確かめます。
Transitionの子要素数とコンポーネントの単一ルート制約
<Transition> の中に <p> を2つ並べると、「<Transition> expects exactly one child element or component.」というコンパイルエラーになります。開発用でも本番用でも同じです。複数並べたいならTransitionGroupを使います。
ルートに置いたTransitionと外側のv-ifの関係
<!-- 動かない:Modalのルートに置いたTransitionは、外側のv-ifでは発火しない -->
<Modal v-if="open" />
<!-- 動く:開閉の状態をpropsで渡し、Transitionの子でv-ifを切り替える -->
<Modal :open="open" />
<script setup>
defineProps({ open: Boolean })
</script>
<template>
<Transition name="fade">
<div v-if="open" class="modal"><slot /></div>
</Transition>
</template>
部品のルートを <Transition> にして親から v-if で消すと、Transitionごと消えるのでleaveは動きません。実測でも外側の v-if ではleaveフックが0回、propsで子の v-if を切り替えたときは1回でした。
再利用Transitionのscopedスタイルとスロットの関係
<Transition> を包んだ共通部品を作るとき、<style scoped> に書いたクラスはスロットで渡された要素に当たりません。公式ガイドもこの場面ではscopedを避けるよう注記しています。スタイルはグローバルに書き、衝突は name で防ぎます。
prefers-reduced-motionで動きを止めるCSSとVueの待ち時間
OSの「視差効果を減らす」設定は、Vueでは自動では反映されません。3.5.45の runtime-dom にこの設定を扱う処理は無く、reduce を指定したChromeでも0.3秒の動きはそのまま再生されました。CSS側で止めます。
@media (prefers-reduced-motion: reduce) {
.fade-enter-active,
.fade-leave-active,
.list-move,
.list-enter-active,
.list-leave-active {
transition: none;
}
}
transition: none なら、Vueは終了を待たずに要素を出し入れします。JavaScriptフックで動かしている場合は window.matchMedia('(prefers-reduced-motion: reduce)').matches で分岐し、動きを省くときも done() を呼んでください。呼ばないとleaveの要素がDOMに残り続けます。
Vue 2からVue 3への移行で変わったトランジションの仕様
Vue 2は2023年12月31日でサポートが終了しています。移行ガイドの変更点は次のとおりです。
| 項目 | Vue 2 | Vue 3 |
|---|---|---|
| enter開始クラス | v-enter |
v-enter-from |
| leave開始クラス | v-leave |
v-leave-from |
| クラス指定props | enter-class・leave-class |
enter-from-class・leave-from-class |
| transition-groupのラッパー | 既定で <span> |
既定で無し |
| ルートのtransition | 外から切り替えで発火 | 発火しない |
CSSは .v-enter を .v-enter-from に、.v-leave を .v-leave-from に置換すれば移行できます。<span> ラッパーにスタイルを当てていたなら tag="span" を追加します。-to と -active のクラス名は変わっていません。
Vue 3.6は2026年10月30日公開の3.6.0-rc.14の段階で、まだ正式版ではありません。通常の仮想DOMの <Transition> はrc.14でも3.5と同じクラスと順番で動きました。Vapor Mode向けには VaporTransition と VaporTransitionGroup が用意されています。
よくある質問
Vueのトランジションとは何ですか?CSSのtransitionとの違いは?
Vueの <Transition> は、要素がDOMに入る・出るタイミングでクラスを付け外しする組み込みコンポーネントです。CSSの transition は値の変化を補間する仕組みで、動きそのものを担います。両者は競合せず、Vueは transition や animation の終了を待ってから要素をDOMから外します。この「消える前に待つ」部分が、CSSだけでは v-if に対して実現できません。
v-appear-fromクラスはVue 3で使えますか?
自動では付きません。appear を付けた初回表示には v-enter-from などのenterクラスが使われます。初回表示専用のクラスを使いたいときは、appear-from-class="v-appear-from" のようにpropsで指定し、対応するCSSを別に書きます。propsを渡さずに .v-appear-from のCSSだけ書いても、そのクラスは要素に付かないので効きません。
TransitionGroupでmodeは使えますか?
使えません。mode は1つの要素を入れ替える場面のための機能で、複数の要素を同時に扱うTransitionGroupには適用されないと公式ガイドに記載があります。リスト内の並べ替えや削除の動きは、-move クラスと -leave-active の position: absolute で作ります。旧要素が消えてから新要素を出したいなら、<Transition> の out-in を使います。
TransitionGroupのtagは必須ですか?
必須ではありません。Vue 3では tag を省くとラッパー要素は描画されず、子要素だけが親の中に並びます。すでに <ul> を書いているなら省略できます。ただし position: absolute の基準が要るので、tag="ul" で付けた要素か外側の要素のどちらかに position: relative を指定してください。Vue 2の既定の <span> に頼っていたコードは tag="span" が要ります。
Nuxtでも同じ書き方で使えますか?
使えます。<Transition> と <TransitionGroup> はVue本体の機能なので、Nuxtの部品の中でそのまま書けます。ページ全体の遷移だけは nuxt.config の app.pageTransition で指定する点が違い、部品のルート要素を1つにする必要があります。Nuxtの全体像はNuxtの解説を参照してください。