En esta página
Transiciones y animaciones en Vue
El sistema de transiciones de Vue
Vue proporciona dos componentes integrados para animar elementos:
<Transition>— Anima la aparición y desaparición de un solo elemento o componente<TransitionGroup>— Anima listas de elementos (añadir, eliminar, reordenar)
La diferencia clave con las animaciones CSS puras es que Vue gestiona automáticamente cuándo aplicar las clases CSS en función del ciclo de vida del elemento.
Cómo funciona Transition
Cuando un elemento dentro de <Transition> entra o sale del DOM (por v-if, v-show, o un cambio de componente dinámico), Vue añade clases CSS en momentos específicos:
Ciclo de entrada:
nombre-enter-from— Estado inicial (antes de aparecer)nombre-enter-active— Toda la duración de la entrada (aquí va la transition/animation CSS)nombre-enter-to— Estado final de la entrada
Ciclo de salida:
nombre-leave-from— Estado inicial de la salidanombre-leave-active— Toda la duración de la salidanombre-leave-to— Estado final (cuando desaparece)
/* Transición "fade" */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
/* Inicio de entrada = mismo que final de salida */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* .fade-enter-to y .fade-leave-from son el estado "visible" — generalmente no necesitan CSS */Transición de página con Vue Router
Las transiciones entre rutas son uno de los usos más comunes:
<!-- App.vue -->
<template>
<RouterView v-slot="{ Component, route }">
<Transition :name="route.meta.transicion ?? 'fade'" mode="out-in">
<component :is="Component" :key="route.path" />
</Transition>
</RouterView>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>Hooks de JavaScript — animaciones con librerías
Además de CSS, <Transition> expone hooks de JavaScript para usar librerías como GSAP:
<script setup lang="ts">
// Necesitarías instalar gsap: npm install gsap
// import gsap from 'gsap'
function alEntrar(el: Element, done: () => void): void {
// gsap.from(el, { opacity: 0, y: 20, duration: 0.3, onComplete: done })
const elemento = el as HTMLElement
elemento.style.opacity = '0'
elemento.style.transform = 'translateY(20px)'
setTimeout(() => {
elemento.style.transition = 'all 0.3s ease'
elemento.style.opacity = '1'
elemento.style.transform = 'translateY(0)'
done()
}, 10)
}
function alSalir(el: Element, done: () => void): void {
const elemento = el as HTMLElement
elemento.style.transition = 'all 0.3s ease'
elemento.style.opacity = '0'
elemento.style.transform = 'translateY(-20px)'
setTimeout(done, 300)
}
</script>
<template>
<Transition
:css="false"
@enter="alEntrar"
@leave="alSalir"
>
<div v-if="visible">Contenido</div>
</Transition>
</template>La prop :css="false" le dice a Vue que no añada las clases CSS automáticas, dejando todo el control a los hooks de JavaScript.
Integración con Animate.css
Animate.css es una popular librería de animaciones CSS:
npm install animate.css// main.ts
import 'animate.css'<template>
<Transition
enter-active-class="animate__animated animate__fadeInDown"
leave-active-class="animate__animated animate__fadeOutUp"
>
<div v-if="visible">Animado con Animate.css</div>
</Transition>
</template>TransitionGroup — listas animadas
<TransitionGroup> es similar a <Transition> pero para listas. Diferencias clave:
- Renderiza un elemento DOM real (por defecto un
<span>, configurable contag) - Cada elemento debe tener un
:keyúnico - Soporta la clase
-movepara animar el reordenamiento
<template>
<TransitionGroup name="lista" tag="div" class="grid">
<TarjetaProducto
v-for="producto in productosFiltrados"
:key="producto.id"
:producto="producto"
/>
</TransitionGroup>
</template>
<style>
/* Las tarjetas aparecen desde abajo */
.lista-enter-from {
opacity: 0;
transform: translateY(30px);
}
/* Las tarjetas se desvanecen hacia arriba al eliminarse */
.lista-leave-to {
opacity: 0;
transform: translateY(-30px);
}
/* Duración de entrada y salida */
.lista-enter-active,
.lista-leave-active {
transition: all 0.4s ease;
}
/* ¡Clave! Las tarjetas restantes se mueven suavemente al reordenarse */
.lista-move {
transition: transform 0.4s ease;
}
/* El elemento que sale debe salir del flujo para no empujar a los demás */
.lista-leave-active {
position: absolute;
}
</style>Transición con appear — animación al montar
La prop appear hace que la transición también se ejecute cuando el componente se monta por primera vez:
<template>
<Transition name="fade" appear>
<div>Este elemento se anima al aparecer por primera vez</div>
</Transition>
</template>Animaciones con CSS custom properties
Puedes usar CSS Custom Properties para hacer las animaciones configurables:
<template>
<Transition name="custom">
<div v-if="visible" :style="{ '--duracion': `${duracion}ms` }">
Animación configurable
</div>
</Transition>
</template>
<style scoped>
.custom-enter-active,
.custom-leave-active {
transition: all var(--duracion, 300ms) ease;
}
.custom-enter-from,
.custom-leave-to {
opacity: 0;
transform: scale(0.9);
}
</style>Animaciones de números — composable useAnimatedNumber
import { ref, watch } from 'vue'
export function useNumeroAnimado(objetivo: () => number, duracion = 500) {
const actual = ref(objetivo())
watch(objetivo, (nuevo, anterior) => {
const inicio = anterior
const fin = nuevo
const tiempoInicio = performance.now()
function animar(ahora: number): void {
const progreso = Math.min((ahora - tiempoInicio) / duracion, 1)
// Easing ease-out
const facilidad = 1 - Math.pow(1 - progreso, 3)
actual.value = Math.round(inicio + (fin - inicio) * facilidad)
if (progreso < 1) requestAnimationFrame(animar)
}
requestAnimationFrame(animar)
})
return actual
}Accesibilidad y animaciones
Siempre respeta la preferencia del usuario por movimiento reducido:
/* En tu archivo global de estilos */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Práctica
- Carrusel con transiciones: Crea un carrusel de imágenes donde cada slide tenga una transición slide-in/slide-out. Usa
mode="out-in"y el estado anterior de la dirección para animar hacia adelante o hacia atrás. - Lista de tareas animada: Añade animaciones con TransitionGroup a una lista de tareas. Las tareas que se añaden deben aparecer desde la derecha, las que se eliminan deben desvanecerse hacia la izquierda, y el resto debe moverse suavemente con la clase
-move. - Toast notifications animados: Crea un sistema de notificaciones toast donde cada toast aparece desde arriba con
TransitionGroup. Las notificaciones se auto-eliminan después de 3 segundos.
En la siguiente lección aprenderemos a escribir tests para nuestros componentes Vue con Vitest y Vue Test Utils.
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(true)
const modo = ref<'fade' | 'slide' | 'scale'>('fade')
</script>
<template>
<div class="controles">
<button type="button" @click="visible = !visible">
{{ visible ? 'Ocultar' : 'Mostrar' }}
</button>
<select v-model="modo">
<option value="fade">Fade</option>
<option value="slide">Slide</option>
<option value="scale">Scale</option>
</select>
</div>
<!-- Transition envuelve UN solo elemento o componente -->
<Transition :name="modo" mode="out-in">
<div v-if="visible" :key="modo" class="caja">
Contenido animado (modo: {{ modo }})
</div>
</Transition>
</template>
<style scoped>
/* Clases generadas por Vue para la transición "fade" */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* Slide */
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-enter-from {
transform: translateX(-20px);
opacity: 0;
}
.slide-leave-to {
transform: translateX(20px);
opacity: 0;
}
/* Scale */
.scale-enter-active,
.scale-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.scale-enter-from,
.scale-leave-to {
transform: scale(0.8);
opacity: 0;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
interface Item {
id: number
texto: string
}
let siguienteId = 4
const items = ref<Item[]>([
{ id: 1, texto: 'Vue 3' },
{ id: 2, texto: 'Pinia' },
{ id: 3, texto: 'Vue Router' },
])
function agregar(): void {
items.value.push({ id: siguienteId++, texto: `Item ${siguienteId}` })
}
function eliminar(id: number): void {
items.value = items.value.filter(i => i.id !== id)
}
function mezclar(): void {
items.value = [...items.value].sort(() => Math.random() - 0.5)
}
</script>
<template>
<div class="controles">
<button type="button" @click="agregar">+ Agregar</button>
<button type="button" @click="mezclar">⇄ Mezclar</button>
</div>
<!-- TransitionGroup anima listas — requiere :key en cada elemento -->
<TransitionGroup name="lista" tag="ul" class="lista">
<li
v-for="item in items"
:key="item.id"
class="item"
>
{{ item.texto }}
<button type="button" @click="eliminar(item.id)">×</button>
</li>
</TransitionGroup>
</template>
<style scoped>
.lista {
list-style: none;
padding: 0;
position: relative; /* Necesario para las animaciones de reordenamiento */
}
.lista-enter-active,
.lista-leave-active {
transition: all 0.4s ease;
}
.lista-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.lista-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* Animar el movimiento de elementos al reordenar */
.lista-move {
transition: transform 0.4s ease;
}
/* Sacar del flujo durante la salida para que los demás se muevan suavemente */
.lista-leave-active {
position: absolute;
width: 100%;
}
</style>
Inicia sesión para guardar tu progreso