Skip to content

Transitions

Advanced

Custom swipe-out and restore animations via the animation prop (Web Animations API).

Demo

Source
vue
<script setup lang="ts">
import type { AnimationContext } from '../../src/utils/animationKeyframes'
import { computed, ref } from 'vue'
import FlashCards from '../../src/FlashCards.vue'
import TransitionCard from './TransitionCard.vue'

type TransitionName = 'fast-rotate' | 'scale-out' | 'flip-3d' | 'elastic-bounce'

const selectedTransition = ref<TransitionName>('fast-rotate')

const cards = [
  { id: 1, title: 'Fast Rotate', description: 'Cards spin rapidly while exiting', color: '#FF6B6B' },
  { id: 2, title: 'Scale to Zero', description: 'Cards shrink to nothing', color: '#4ECDC4' },
  { id: 3, title: '3D Flip', description: 'Cards flip in 3D space', color: '#45B7D1' },
  { id: 4, title: 'Elastic Bounce', description: 'Cards bounce with elastic effect', color: '#FFA726' },
  { id: 5, title: 'Custom Effects', description: 'Combine multiple animations', color: '#AB47BC' },
]

/**
 * Horizontal exit offset, signed by direction. Cards only swipe left/right in
 * this demo, so we build the off-screen X from the action type.
 */
function exitX(type: AnimationContext['type']): number {
  return type === 'left' ? -300 : 300
}

/**
 * Each transition describes ONLY the fly-out (the off-screen end frame), from
 * center. The library starts it from the drag-release point and plays it
 * reversed for restore — we don't write either here.
 */
const transitions: Record<TransitionName, (ctx: AnimationContext) => Keyframe> = {
  'fast-rotate': (ctx) => {
    const deg = ctx.type === 'left' ? -360 : 360
    return { transform: `translateX(${exitX(ctx.type)}px) rotate(${deg}deg)`, opacity: 0 }
  },
  'scale-out': ctx => ({ transform: `translateX(${exitX(ctx.type)}px) scale(0)`, opacity: 0 }),
  'flip-3d': (ctx) => {
    const y = ctx.type === 'left' ? -180 : 180
    return { transform: `translateX(${exitX(ctx.type)}px) rotateY(${y}deg) rotateX(45deg)`, opacity: 0 }
  },
  'elastic-bounce': (ctx) => {
    const deg = ctx.type === 'left' ? -15 : 15
    return { transform: `translateX(${exitX(ctx.type)}px) scale(1.3) rotate(${deg}deg)`, opacity: 0 }
  },
}

// Per-transition timing/easing, applied alongside the keyframes.
const timing: Record<TransitionName, { duration: number, easing: string }> = {
  'fast-rotate': { duration: 400, easing: 'linear' },
  'scale-out': { duration: 300, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' },
  'flip-3d': { duration: 500, easing: 'ease-in-out' },
  'elastic-bounce': { duration: 600, easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)' },
}

const animation = computed(() => ({
  keyframes: transitions[selectedTransition.value],
  duration: timing[selectedTransition.value].duration,
  easing: timing[selectedTransition.value].easing,
}))
</script>

<template>
  <div class="max-w-lg mx-auto p-6 transition-effects-example">
    <div class="text-center mb-8">
      <h2 class="text-3xl font-bold text-base-content mb-2">
        Custom Transition Effects
      </h2>
      <p class="text-base-content/70">
        Experiment with different transition styles
      </p>
    </div>

    <div class="bg-base-200 p-6 rounded-2xl mb-8">
      <div class="grid grid-cols-2 gap-4">
        <label class="label cursor-pointer">
          <input
            v-model="selectedTransition"
            type="radio"
            value="fast-rotate"
            class="radio radio-primary"
          >
          <span class="label-text ml-2">Fast Rotate</span>
        </label>
        <label class="label cursor-pointer">
          <input
            v-model="selectedTransition"
            type="radio"
            value="scale-out"
            class="radio radio-secondary"
          >
          <span class="label-text ml-2">Scale to Zero</span>
        </label>
        <label class="label cursor-pointer">
          <input
            v-model="selectedTransition"
            type="radio"
            value="flip-3d"
            class="radio radio-accent"
          >
          <span class="label-text ml-2">3D Flip</span>
        </label>
        <label class="label cursor-pointer">
          <input
            v-model="selectedTransition"
            type="radio"
            value="elastic-bounce"
            class="radio radio-info"
          >
          <span class="label-text ml-2">Elastic Bounce</span>
        </label>
      </div>
    </div>

    <div class="h-96 w-full relative">
      <FlashCards
        :items="cards"
        :animation="animation"
        loop
      >
        <template #default="{ item }">
          <TransitionCard :card="item" />
        </template>
      </FlashCards>
    </div>
  </div>
</template>
vue
<script setup lang="ts">
interface Card {
  id: number
  title: string
  description: string
  color: string
}

defineProps<{
  card: Card
}>()
</script>

<template>
  <div
    class="card size-full text-white shadow-xl"
    :style="{ backgroundColor: card.color }"
  >
    <div class="card-body">
      <h2 class="card-title">
        {{ card.title }}
      </h2>
      <p>{{ card.description }}</p>
      <div class="card-actions justify-end">
        <div class="badge badge-outline">
          #{{ card.id }}
        </div>
      </div>
    </div>
  </div>
</template>

Key Concepts

  • animation: a single object prop — { keyframes, duration, easing } (all optional)
  • animation.keyframes: a function returning the off-screen fly-out frame
  • AnimationContext: type, direction, maxRotation
  • Per-direction styles: branch on ctx.type for left/right/top/bottom
  • One frame, both ways: the library starts the swipe from the drag-release point and plays it reversed for restore
  • Timing: animation.duration / animation.easing

Common Patterns

ts
const animation = {
  duration: 400,
  keyframes(ctx) {
    const x = ctx.type === 'left' ? -300 : 300

    // Just the off-screen end frame:
    return { transform: `translateX(${x}px) rotate(360deg)`, opacity: 0 }
    // Scale to zero:  { transform: `translateX(${x}px) scale(0)`, opacity: 0 }
    // 3D flip:        { transform: `translateX(${x}px) rotateY(180deg)`, opacity: 0 }
  },
}

Released under the MIT License.