Skip to example

AnimateView: Transition types

A direction-aware image carousel that reads transition types to slide images in from the correct side.

Vue

Source code

<script setup lang="ts">
import { AnimateView, spring, startTransition, wrap } from 'motion-v'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
import { ref } from 'vue'

const images = Array.from(
  { length: 6 },
  (_, i) => `/photos/prague/image-${String(i + 1).padStart(2, '0')}.jpg`,
)

// Preload so slides don't pop in mid-transition
images.forEach((src) => {
  const img = new Image()
  img.src = src
})

const index = ref(0)

const slideSpring = {
  type: spring,
  visualDuration: 0.3,
  bounce: 0.2,
}

function navigate(direction: 'next' | 'prev') {
  startTransition(
    () => {
      index.value = wrap(
        0,
        images.length,
        index.value + (direction === 'next' ? 1 : -1),
      )
    },
    { types: [direction] },
  )
}

function enterSlide(types: string[]) {
  return {
    opacity: 1,
    transform: [
      `translateX(${types.includes('next') ? 100 : -100}%)`,
      'translateX(0%)',
    ],
  }
}

function exitSlide(types: string[]) {
  return {
    opacity: 0,
    transform: `translateX(${types.includes('prev') ? 100 : -100}%)`,
  }
}
</script>

<template>
  <div class="types-example">
    <button
      class="nav-button"
      aria-label="Previous"
      @click="navigate('prev')"
    >
      <ChevronLeft :size="24" />
    </button>
    <AnimateView
      :key="index"
      :name="`slide-${index}`"
      :transition="slideSpring"
      :exit="exitSlide"
      :enter="enterSlide"
    >
      <div class="slide">
        <img
          :src="images[index]"
          :alt="`Prague ${index + 1}`"
          class="slide-image"
        >
      </div>
    </AnimateView>
    <button
      class="nav-button"
      aria-label="Next"
      @click="navigate('next')"
    >
      <ChevronRight :size="24" />
    </button>
    <span class="counter">{{ index + 1 }} / {{ images.length }}</span>
  </div>
</template>

<style scoped>
.types-example {
  display: flex;
  position: relative;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  width: 100%;
  height: 100%;
}

.counter {
  width: 100%;
  text-align: center;
  font-size: 14px;
  color: var(--feint-text);
}

.slide {
  width: 400px;
  height: 300px;
  border-radius: 10px;
  overflow: hidden;
}

.slide-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.nav-button {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  z-index: 1;
  outline-offset: 2px;
  border: none;
  cursor: pointer;
  color: white;
  background-color: rgba(255, 255, 255, 0.15);
}

.nav-button:hover {
  background-color: rgba(255, 255, 255, 0.25);
}
</style>