🃏
VueのTransitionを駆使してカードめくりアニメーションを作る
デモ 兼 結論
(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()
を指定すれば良さそうだなぁ
- Transition は 要素の挿入・削除時に働くから、「表の要素を削除→裏の要素を挿入」「裏の要素を削除→表の要素を挿入」とすればよさそうだなぁ
- カードの裏面と表面はハードコーディングせず、コンポーネントにして、随時に指定できるようにしたいなぁ
- ということは Slot を使うとよさそうだなぁ
実装(段階を踏んで)
Step1: とりあえずガッと
仕様
JS的な
- 表示面を定める変数
side
を持つ。取りうる値はA
もしくはB
-
side
がA
のとき、 SlotsideA
に渡されたコンポーネントのみを表示し、SlotsideB
に渡されたコンポーネントは非表示とする -
side
がB
のとき、 SlotsideB
に渡されたコンポーネントのみを表示し、SlotsideA
に渡されたコンポーネントは非表示とする
-
-
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>
- *1) 要素間のトランジション -- トランジション | Vue.js
- *2) トランジションクラス -- トランジション | Vue.js
- 出現する要素の
- 出現してくるとき:
v-enter-from
- これの持続時間とかの挙動:
v-enter-acive
- これの持続時間とかの挙動:
- 出現し終わった瞬間:
v-enter-to
- 出現してくるとき:
- 消失する要素の
- 消失し始める瞬間:
v-leave-from
- 消失していくとき:
v-leave-to
- これの持続時間とかの挙動:
v-leave-acive
- これの持続時間とかの挙動:
- 消失し始める瞬間:
- 出現する要素の
成果物
古い面と新しい面が同時に表示され、せり上がるように表示されてしまった図
違う違うそうじゃない
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 を指定することにより、アニメーションのタイミングを「消失・出現の同時発生」から「消失→出現」の順や「出現→消失」の順に変更できる
-
登場したリンク
要素間のトランジション -- トランジション | Vue.js
トランジションクラス -- トランジション | Vue.js
トランジションモード -- トランジション | Vue.js
Discussion