🃏

VueのTransitionを駆使してカードめくりアニメーションを作る

2022/12/16に公開

デモ 兼 結論

(StackBlitzのサードパーティCookieを許可しないと動かないです)

前置き

この記事を読むとわかること

  • Vue.jsでTransitionを使ったカードめくりアニメーションの作り方
  • Transition、特に以下の機能について
    • v-if および v-else / v-else-if によって排他になる場合に限り、複数要素を配置することができる
    • Transition mode を指定することにより、アニメーションのタイミングを「消失・出現の同時発生」から「消失→出現」の順や「表示→出現」の順に変更できる

わからないこと

  • Slotについて
    • リンクを貼りました。この記事では説明しません。

手元の環境

  • macOS Ventura 13.0.1
  • Node.js 18.8.x
  • Vue.js 3.2.x

たぶん、 Vue の2系でも動きます。

本編

ふだん生活していると、急に神経衰弱を作りたくなることが年に1回くらいあります。
カードをクリックしてパッと切り替わっても芸がないので、アニメーションがほしいですね。

作戦

  • Vue でアニメーションといえば Transition だなぁ
    • Transition は 要素の挿入・削除時に働くから、「表の要素を削除→裏の要素を挿入」「裏の要素を削除→表の要素を挿入」とすればよさそうだなぁ
      • v-if で切り替えれば良さそうだなぁ
    • 動き自体は CSS で transform: rotateY() を指定すれば良さそうだなぁ

https://ja.vuejs.org/guide/built-ins/transition.html

  • カードの裏面と表面はハードコーディングせず、コンポーネントにして、随時に指定できるようにしたいなぁ
    • ということは Slot を使うとよさそうだなぁ

https://ja.vuejs.org/guide/components/slots.html

実装(段階を踏んで)

Step1: とりあえずガッと

仕様

JS的な
  • 表示面を定める変数 side を持つ。取りうる値は A もしくは B
    • sideA のとき、 Slot sideA に渡されたコンポーネントのみを表示し、Slot sideB に渡されたコンポーネントは非表示とする
    • sideB のとき、 Slot sideB に渡されたコンポーネントのみを表示し、Slot sideA に渡されたコンポーネントは非表示とする
  • side は、要素のクリック時に相互に切り替わる
CSS的な
  • 切り替わりのとき、
    • 消失する要素は、Y軸方向に90度回転してから消失する
    • 出現する要素は、Y軸方向に90度回転してから出現する
    • アニメーションの所要時間は適当

コード

TransitionFlip.vue
<script setup lang="ts">
import { ref } from 'vue'

type Side = 'A' | 'B'

// 表示面を決める ref と、それを切り替える関数
const side = ref<Side>('A')
const handleClick = () => {
  const next = side.value === 'A' ? 'B' : 'A'
  side.value = next
}
</script>

<template>
  <Transition
    @click="handleClick"
  >
    <!--
      *1) Transition には通常単一の要素しか入れられないが、
      v-if / v-else / v-else-if によって排他にできる場合に限り、
      複数の要素を入れることができる
    -->
    <slot
      name="sideA"
      v-if="side === 'A'"
    />
    <slot
      name="sideB"
      v-else
    />
  </Transition>
</template>

<style scoped>
/* *2) Transition 特有のクラス */

/* transformに指定した内容を300ミリ秒で動かす */
.v-enter-active,
.v-leave-active {
  transition: transform 300ms;
}

/* 出現してくるとき、消失していくときにY軸方向に90度回転させる */
.v-enter-from,
.v-leave-to {
  transform: rotateY(90deg);
}
</style>
使い方
<template>
  <TransitionFlip>
    <template #sideA>
      <!-- ここに任意の要素を入れる -->
      <div class="cardA">A</div>
    </template>
    <template #sideB>
      <!-- ここに任意の要素を入れる -->
      <div class="cardB">B</div>
    </template>
  </TransitionFlip>
</template>

成果物


古い面と新しい面が同時に表示され、せり上がるように表示されてしまった図

違う違うそうじゃない

Step2: 消失・出現のアニメーションの順序を守らせる

先ほど失敗した原因は、 消失と出現が同時に起こっていた ためです。
そうすると同時にA面・B面両方のDOMが存在するタイミングが発生し、先のような表示になります。

もちろんこれの対策が存在します。トランジションモードです。

トランジションモード -- トランジション | Vue.js

これは、 片方のアニメーションが完了した後に、もう片方のアニメーションを実行する という代物です。
Transition の mode プロパティに、 out-in (消失→出現) / in-out (出現→消失) を指定することで有効化できます。

今回は、「消失→出現」の順にしたいので、 out-in を使用します。

コード

TransitionFlip.vue
 <script setup lang="ts">
 // ry
 </script>
 
 <template>
  <Transition
    @click="handleClick"
+   mode="out-in"
  >
    <!-- ry -->
  </Transition>
 </template>
 
 <style lang="scss">
 /* ry */
 </style>

成果物


いい感じにめくったように見える図

いい感じですね。

Step 3(おまけ): 外部から表示面を指定・切り替えできるようにする

神経衰弱を作るとなると、親コンポーネントから表示面を指定したくなるかもしれません。
propsを渡して、表示面と同期するようにしましょう。
propsを渡さなくても動くようにしたいので、もともとあったrefの side は活かします。

TransitionFlip.vue
 <script setup lang="ts">
+ import { onMounted, ref, toRef, watch } from 'vue';

  type Side = 'A' | 'B';
+ const props = defineProps<{ side?: Side }>();
+ // propsはリアクティブオブジェクト(プロパティ自体はリアクティブじゃない)。
+ // 今回はsideにしか興味がないので、 toRef を使ってプロパティ自体をリアクティブにしてしまう。
+ const propSide = toRef(props, 'side');
+ const emit = defineEmits<{
+   (e: 'update:side', value: Side): void;
+ }>();

  const sideRef = ref<Side>('A');
  const handleClick = () => {
    const next = sideRef.value === 'A' ? 'B' : 'A';
+   // refの値が変わったらpropsを書き換えるイベントを発行する
+   emit('update:side', next);
    sideRef.value = next;
  };
 
+ // マウント時にpropsの値をrefに入れる
+ onMounted(() => {
+   if (props.side === undefined) return;
+   sideRef.value = props.side;
+ });

+ // propsの値が変わったらrefに入れる
+ watch(propSide, (newSide) => {
+   if (newSide === undefined) return;
+   sideRef.value = newSide;
+ });
 </script>
使い方
+ <script setup lang="ts">
+ import { ref } from 'vue';
+ const side = ref<'A' | 'B'>('A');
+ </script>
 <template>
+  <!-- v-model最高!!! --> 
+  <TransitionFlip v-model:side="side">
    <template #sideA>
      <!-- ここに任意の要素を入れる -->
      <div class="cardA">A</div>
    </template>
    <template #sideB>
      <!-- ここに任意の要素を入れる -->
      <div class="cardB">B</div>
    </template>
  </TransitionFlip>
+  <div>
+    <!-- この辺は適当に -->
+    <button @click="handleClick">flip</button>
+    <br />
+    side: {{ side }}
+  </div>
 </template>

成果物


propsと同期している図

まとめ

  • Transition、特に以下の機能について
    • v-if および v-else / v-else-if によって排他になる場合に限り、複数要素を配置することができる
    • Transition mode を指定することにより、アニメーションのタイミングを「消失・出現の同時発生」から「消失→出現」の順や「出現→消失」の順に変更できる

登場したリンク

https://ja.vuejs.org/guide/built-ins/transition.html

要素間のトランジション -- トランジション | Vue.js
トランジションクラス -- トランジション | Vue.js
トランジションモード -- トランジション | Vue.js

https://ja.vuejs.org/guide/components/slots.html

GitHubで編集を提案

Discussion