Vue.js でたのしいトランジション 通常編

最終更新日
2018.02.06

雰囲気で使うのを卒業するべく少し詳しく調べてみました。

凝った自作のアニメーション遷移を作る場合デザインと JavaScript 両方の知識が必要になる事が多いですが Vue のトランジション機能 を使うと比較的簡単に作ることが出来ます。

トランジションで付加されるクラスが色々あって複雑なアニメーション CSS を作りたい時「どのクラスにどういうスタイルをつければ良いんだっけ?」と迷ってしまう事があると思いますが、クラスの付くタイミングを覚えておくととても楽になります。

クラス名とプレフィックス

Vue の トランジションは CSS の transitionanimation プロパティが使えますが、この記事は基本の transition を使ったパターンで書いています。

トランジションは切り替わる要素を <transition></transition> タグで囲むだけで使用できます。display:none や DOM からの削除は Vue がやってくれるので基本は CSS アニメーションを作るだけ。

<transition>
  <div>トランジションさせたい要素</div>
</transition>

表示される時(enter)消える時(leave)それぞれのクラス名を使用できるようになります。クラス名はタグのオプションで名前を設定しない場合デフォルトで v- というプレフィックスが付きます。scoped CSS を使う場合は名前の被りを気にしなくていいのでデフォルトで良いかも。

.v-enter-active, .v-leave-active {
  transition: opacity 1s;
}
.v-enter, .v-leave-to {
  opacity: 0;
}

名前をつけた場合はその名前がプレフィックスになります。

<transition name="demo">
  <div>トランジションさせたい要素</div>
</transition>

この場合は v- の部分が demo- になります。

.demo-enter-active, .demo-leave-active {
  transition: opacity 1s;
}
.demo-enter, .demo-leave-to {
  opacity: 0;
}

クラスの種類とトランジション中のクラスの状態

トランジションの内部では requestAnimationFramesetTimeout を使って非同期なフレーム処理がされてるみたいです。フレームとクラスの付加・削除は Flash や AfterEffects のタイムラインとキーフレームみたいなものですね。

公式の画像と大体一緒ですが少し詳しく画像とアニメーションを作りました。以下の例は7フレームを使う俊足で単純なアニメーションのクラスの状態です。2フレーム以降は雰囲気なので7分割と言うのはサイズ的に丁度良かっただけです。

enter 非表示から表示

  • トランジション開始 v-enter & v-enter-active 付いた状態で要素が追加される
  • 次のフレームで v-enter が削除されて v-enter-to が追加される
  • トランジション終了 v-enter-activev-enter-to が削除される

leave 表示から非表示

  • トランジション開始 v-leavev-leave-active が追加される
  • 次のフレームで v-leave が削除されて v-leave-to が追加される
  • トランジション終了 v-leave-active & v-leave-to が削除され要素が削除される

アニメーションで見る

これは目視しやすいように1fpsにしてます。

enter の場合だと DOM がない状態で v-enter/v-enter-active が付くためすぐにその状態に変化して、次のフレームで DOM への追加をして描画されます。2フレーム目で v-enter が削除されて v-enter-to になったところでディレイとデュレーションを含んだ opacity:0 から opacity:1 へのCSSトランジションが始まります。

トランジションを作る

アニメーションを作る時の基本は v-enter/v-leave から v-enter-to/v-leave-to への CSSトランジションです。v-enter-active/v-leave-activetransition プロパティの指定と JavaScript で何か操作する時に使うぐらいで、このクラスに装飾的なスタイルを設定する必要はほぼありません。

おうちゃくして対象の要素(.item)に直接 CSS トランジションを all で設定してますが .v-enter-active, .v-leave-active に設定したほうがホバー用など他のアニメーションに影響しません。

おうちゃくする場合

.item {
  transition: all 1s;
}

ちゃんと書く場合

.v-enter-active, .v-leave-active {
  transition: opacity 1s;
}

基本的なトランジション

demo1 はフェードイン&フェードアウトの簡単なトランジションです。

/* トランジション中有効にする CSS transition プロパティ */
.demo1-enter-active, .demo1-leave-active {
  transition: opacity 1s;
}
/* 表示される時は0から1へ*/
.demo1-enter {
  opacity: 0;
}
.demo1-enter-to {
  opacity: 1;
}
/* 消える時は1から0へ*/
.demo1-leave {
  opacity: 1;
}
.demo1-leave-to {
  opacity: 0;
}

元々何も付いていなければ opacity:1 の初期状態なので demo1-enter-todemo1-leave は省略できます。また、demo1-enterdemo1-leave-to はスタイルが同じでまとめられるのでこんな感じに書けますね(๑'ᴗ'๑) こうすると公式のサンプルと同じになりました。

.demo1-enter-active, .demo1-leave-active {
  transition: opacity 1s;
}
.demo1-enter, .demo1-leave-to {
  opacity: 0;
}

表示/非表示で状態が違うトランジション

demo2 は 表示と非表示で動きを変えたトランジションです。

/* 遷移後に文字色をアニメーションで戻すために要素自体にも設定 */
.item {
  transition: color 2s;
}
/* 表示するときはゆっくり@文字色だけアニメーションさせない */
/* 遷移中は文字色を透明にしとく */
.demo2-enter-active {
  color: transparent;
  transition: all 1s, color 0s;
}
/* 消えるときは素早く */
.demo2-leave-active {
  transition: all .5s;
}
/* 表示するときは透明度0の左側から */
.demo2-enter {
  opacity: 0;
  transform: translateX(-50px);
}
/* 消えるときは透明度0の左側へ */
.demo2-leave-to {
  opacity: 0;
  transform: translateX(50px);
}

表示される時は左からゆっくりスライドして消える時は素早く右にスライドするようになります。

遷移が終わったあとのアニメーションは他に影響しなければこれでもいいですが、ホバー時のアニメーションでは速さを 1s にしたいとか細かく設定したい場合は要素自体に付けるよりもタイマーを設定して Vue と同じようにクラスを付けるほうが影響しないかなと思います。

スライドで入れ替わるトランジション

demo3 は複数のコンポーネントを入れ替えるトランジションです。

.demo3-enter-active, .demo3-leave-active {
  transition: all 1s;
}
.demo3-enter {
  opacity: 0;
  transform: translateX(-100px) scale(1.2);
}
.demo3-leave-to {
  opacity: 0;
  transform: translateX(100px) scale(0.8);
}
.demo3-leave-active {
  position: absolute;
}

消す時に position:absolute にして重ねておいて transform で見える位置をずらしています。こんなのも簡単にできる!ルータービューとかに使うとチョットいい感じ。

同じ要素をトランジションする場合はユニークなキーを設定します。

<transition name="demo3">
  <div class="item" v-if="current==0" key="0">item1</div>
  <div class="item" v-if="current==1" key="1">item2</div>
  <div class="item" v-if="current==2" key="2">item3</div>
</transition>

これは複数の要素を使っていても結果は常に1つなので通常の transition が使えますが、結果が複数の要素になる場合は transition-group を使います。

トランジションフック

フックを使うとトランジションに処理を挟んだりスクリプトで自由なアニメーションのロジックを組み立てる事ができる。

クラスとフックのタイミングはこのソースコードを見るとよく分かります。

.../runtime/modules/transition.js

CSS用クラスとフックを一緒に使うこともできるけど :css="false" を設定すると Vue 側でクラスとフレームの処理がされないため完全に JavaScript で制御したい場合は都合がいい。ちなみにその場合は要素の追加/削除以外の変化は無く、各フックも同期なのでクラスを付けても setTimeoutrequestAnimationFrame を使って自分でキーフレームを打つ処理をしないと CSS トランジションも開始されません。

before-enter / enter / after-enter / enter-cancelled

  • before-enter DOM に要素が追加される前、重複でなければクラスが付く前
  • enter .v-enter が付いて DOM に要素が追加された後、CSS:false なら before-enter の直後
  • after-enter enterdone() したら
  • enter-cancelled 重複して途中キャンセルした時

before-leave / leave / after-leave / leave-cancelled

  • before-leave 重複でなければクラスが付く前
  • leave .v-leave が付いた後、CSS:false なら before-leave の直後
  • after-leave leavedone() した後で DOM から要素が削除された後
  • leave-cancelled 重複して途中キャンセルした時 (v-showを使った時のみ。他はその代わりかどうかはワカラナイですがafter-leaveが発生)

確認してみた所だいたいこんな感じかなと思います。after-enter/after-leaveCSS:false に設定して enter/leave のフックを使用している場合 done() しない限り発生しない。enter/leaveを使っていなければ即座に発生。

最初に表示するときにも再生したい場合

<transition></transition> タグに appear を付けるだけでOK。最初に表示するときもトランジションされるようになる。

<transition appear>
  <div v-if="fade">...</div>
</transition>

何も設定しない場合 v-enter/v-leave のクラスやフックが使われますが、初期表示を区別して何かしたい場合は名前をカスタマイズ出来ます。

トランジションで高さが変わる時にアニメーションする

これは状態のトランジションなので Vue の transition タグとは別に設定します。非同期コンテンツが入ると結構めんどくさいですが、色々方法はあると思うんですけどサンプルはラッパー要素とウォッチャを使った例です。

ウォッチャを使ってアニメーションする場合

<div id="app">
  <p><button @click="current=current==2?1:2">切り替える</button></p>
  <div id="box" ref="box">
    <div ref="boxBody">
      <transition name="demo4" @after-enter="removeHeight">
        <div class="item" key="1" v-if="current==1">item1</div>
        <div class="item" key="2" v-if="current==2" style="height:50px;">item2</div>
      </transition>
    </div>
  </div>
  <div>bottom text</div>
</div>
.demo4-enter-active, .demo4-leave-active {
  transition: all 1s;
}
.demo4-enter, .demo4-leave-to {
  opacity: 0;
}
.demo4-leave-active {
  position: absolute;
}
#box {
  transition: height 1s;
  overflow: hidden;
  border: 2px solid #ccc;
  padding: 10px;
}
let height = ''
new Vue({
  el: '#app',
  data: {
    current: 1,
  },
  watch: {
    current: {
      handler() {
        this.$nextTick(() => {
          // 変化があったら今の高さを設定
          this.$refs.box.style.height = height + 'px'
          // 再取得して設定
          height = this.$refs.boxBody.getBoundingClientRect().height
          this.$refs.box.style.height = height + 'px'
        })
      },
      immediate: true
    }
  },
  methods: {
    // 後から高さが変化してもいいようにアニメーションが終わったら高さ指定を消す@フック使用
    removeHeight() {
      this.$refs.box.style.height = ''
    }
  }
})

なんとか CSS だけでやる

一応ラッパーの方にトランジションを施す事で高さをアニメーションする方法もあります。max-height も使うとある程度の可変サイズも JavaScript を書かずに CSS だけで出来るけどデュレーションやイージングが指定通りにならないのと CSS もややこしくなるのであまりオススメできないかも。

<transition name="demo">
  <div class="wrapper" v-if="current">
    <div class="item">item</div>
  </div>
</transition>
.wrapper {
  transition: height 0.5s;
  height: 100px;
}
.item {
  transition: opacity 1s;
}
.demo1-enter, .demo1-leave-to {
  height: 0px;
  overflow: hidden;
}
.demo1-enter .item, .demo1-leave-to .item {
  opacity: 0;
}

表示したまま状態を変えたい場合

一つの要素に enter/leave 両方の状態を通して変化させるという事は出来ない。(要素がコピーされてしまう)単純に色や位置などの状態を変えたい場合は CSS の transition を設定するだけで可能なパターンが多いです。それ以外は公式マニュアルにある 状態のトランジション の方法を使ったり、ウォッチャやカスタムディレクティブを使って自分で要素のクラスやスタイルを変更すると良いです。

まとめ

あんまりゴチャゴチャしてるのもあれですけどちょっと動きを付けると楽しいですね!

ところで Vue のトランジションタグは SVG の中の要素にも使うことが出来るんですよ。便利(っ'ロ'c)❤ この記事の説明で使っている操作可能な SVG アニメーションも Vue で作っています。

次回たのしいトランジションリスト編 でぬるぬる動くリストの作り方も書いてるのでぜひ見てください。