Transition con CSS
El componente Transition de Vue aplica animaciones de entrada y salida cuando un elemento o componente se inserta o elimina del DOM. Funciona con v-if, v-show, componentes dinámicos con <component :is> y la transición del componente raíz de una ruta.
Transition envuelve un único elemento o componente hijo y aplica clases CSS en momentos específicos del ciclo de entrada/salida.
Clases de transición CSS
Vue genera automáticamente seis clases CSS durante una transición:
Entrada:
v-enter-from: Estado inicial antes de entrar. Se aplica un frame antes de la inserción y se elimina un frame después.v-enter-active: Estado activo durante toda la entrada. Se aplica durante toda la fase de entrada. Aquí se definentransitionoanimation.v-enter-to: Estado final de la entrada. Se aplica un frame después de la inserción y se elimina cuando la transición termina.
Salida:
v-leave-from: Estado inicial de la salida. Se aplica inmediatamente cuando se dispara la salida.v-leave-active: Estado activo durante toda la salida. Similar av-enter-activepara la fase de salida.v-leave-to: Estado final de la salida. Se aplica un frame después del inicio de la salida.
Ejemplo básico:
<script setup lang="ts">
import { ref } from 'vue'
const show = ref(true)
</script>
<template>
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">Este texto aparece y desaparece con animación</p>
</Transition>
</template>
<style>
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
Transiciones con nombre
Cuando se usa la prop name, las clases usan ese nombre como prefijo en lugar de v-:
<template>
<Transition name="slide-fade">
<div v-if="show" class="panel">Contenido del panel</div>
</Transition>
</template>
<style>
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.2s ease-in;
}
.slide-fade-enter-from {
transform: translateX(-20px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
</style>
Con el nombre slide-fade, las clases generadas son slide-fade-enter-from, slide-fade-enter-active, slide-fade-enter-to, slide-fade-leave-from, slide-fade-leave-active y slide-fade-leave-to.
Clases de transición personalizadas
Las props de clases personalizadas permiten usar clases de librerías externas como Animate.css:
<template>
<Transition
enter-active-class="animate__animated animate__fadeInUp"
leave-active-class="animate__animated animate__fadeOutDown"
>
<div v-if="show" class="card">Contenido animado</div>
</Transition>
</template>
Las props disponibles son: enter-from-class, enter-active-class, enter-to-class, leave-from-class, leave-active-class y leave-to-class. Esto sobrescribe las clases generadas automáticamente por Vue.
Modos de transición
Cuando se alterna entre dos elementos, ambos se animan simultáneamente por defecto. La prop mode controla el orden:
mode="out-in": El elemento actual sale primero y luego el nuevo entra. Este es el modo más usado.mode="in-out": El nuevo elemento entra primero y luego el actual sale.
<script setup lang="ts">
import { ref } from 'vue'
const isEditing = ref(false)
</script>
<template>
<Transition name="fade" mode="out-in">
<div v-if="isEditing" key="edit">
<input type="text" placeholder="Editando..." />
<button @click="isEditing = false">Guardar</button>
</div>
<div v-else key="display">
<p>Contenido en modo lectura</p>
<button @click="isEditing = true">Editar</button>
</div>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
El atributo key es necesario para que Vue distinga entre los dos elementos y aplique la transición correctamente.
JavaScript hooks
Transition emite eventos JavaScript en cada fase de la animación, permitiendo crear animaciones programáticas con librerías como GSAP o Web Animations API:
<script setup lang="ts">
import { ref } from 'vue'
const show = ref(true)
function onBeforeEnter(el: Element) {
const htmlEl = el as HTMLElement
htmlEl.style.opacity = '0'
htmlEl.style.transform = 'translateY(30px)'
}
function onEnter(el: Element, done: () => void) {
const htmlEl = el as HTMLElement
htmlEl.animate(
[
{ opacity: 0, transform: 'translateY(30px)' },
{ opacity: 1, transform: 'translateY(0)' }
],
{ duration: 400, easing: 'ease-out' }
).onfinish = done
}
function onLeave(el: Element, done: () => void) {
const htmlEl = el as HTMLElement
htmlEl.animate(
[
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.9)' }
],
{ duration: 300, easing: 'ease-in' }
).onfinish = done
}
</script>
<template>
<button @click="show = !show">Toggle</button>
<Transition
:css="false"
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
>
<div v-if="show" class="animated-box">Contenido animado con JS</div>
</Transition>
</template>
Los hooks disponibles son: @before-enter, @enter, @after-enter, @enter-cancelled, @before-leave, @leave, @after-leave y @leave-cancelled. La prop :css="false" indica a Vue que no aplique clases CSS automáticamente y confíe en los hooks JavaScript.
La función done del hook @enter y @leave debe llamarse para indicar que la animación ha terminado.
Animación inicial con appear
La prop appear aplica la transición cuando el componente se monta por primera vez:
<template>
<Transition name="fade" appear>
<h1>Este título se anima al cargar la página</h1>
</Transition>
</template>
<style>
.fade-enter-active {
transition: opacity 0.6s ease;
}
.fade-enter-from {
opacity: 0;
}
</style>
Sin appear, la transición solo se aplica cuando el elemento se inserta o elimina dinámicamente después del montaje.
TransitionGroup para listas
TransitionGroup aplica transiciones a los elementos de una lista renderizada con v-for. A diferencia de Transition:
- Renderiza un elemento wrapper real en el DOM (por defecto
<span>, configurable con la proptag) - No soporta la prop
mode - Cada elemento hijo debe tener un atributo
:keyúnico - Las clases de transición se aplican a cada elemento individual de la lista
<script setup lang="ts">
import { ref } from 'vue'
interface ListItem {
id: number
text: string
}
const items = ref<ListItem[]>([
{ id: 1, text: 'Primer elemento' },
{ id: 2, text: 'Segundo elemento' },
{ id: 3, text: 'Tercer elemento' }
])
let nextId = 4
function addItem() {
const index = Math.floor(Math.random() * (items.value.length + 1))
items.value.splice(index, 0, {
id: nextId++,
text: `Elemento ${nextId - 1}`
})
}
function removeItem(id: number) {
items.value = items.value.filter(item => item.id !== id)
}
</script>
<template>
<button @click="addItem">Añadir</button>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id" @click="removeItem(item.id)">
{{ item.text }}
</li>
</TransitionGroup>
</template>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.4s ease;
}
.list-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-leave-active {
position: absolute;
}
.list-move {
transition: transform 0.4s ease;
}
</style>
Transiciones de movimiento (FLIP)
La clase *-move es exclusiva de TransitionGroup y se aplica cuando los elementos cambian de posición en la lista. Vue usa la técnica FLIP (First, Last, Invert, Play) internamente:
- First: Registra la posición actual de cada elemento
- Last: Calcula la posición final después del cambio
- Invert: Aplica un
transformpara que los elementos parezcan estar en su posición original - Play: Elimina el
transformcon una transición CSS, animando el movimiento
Para que la animación de movimiento funcione correctamente cuando un elemento sale, este debe sacarse del flujo normal con position: absolute en la clase *-leave-active. Esto permite que los elementos restantes se reposicionen inmediatamente y la transición de movimiento se aplique.
Ejemplo completo: lista animada de tareas
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Todo {
id: number
text: string
completed: boolean
}
const newTodoText = ref('')
const todos = ref<Todo[]>([])
let nextId = 1
const sortedTodos = computed(() =>
[...todos.value].sort((a, b) => Number(a.completed) - Number(b.completed))
)
function addTodo() {
if (newTodoText.value.trim() === '') return
todos.value.push({
id: nextId++,
text: newTodoText.value.trim(),
completed: false
})
newTodoText.value = ''
}
function removeTodo(id: number) {
todos.value = todos.value.filter(t => t.id !== id)
}
function toggleTodo(id: number) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}
</script>
<template>
<div class="todo-app">
<form @submit.prevent="addTodo" class="add-form">
<input v-model="newTodoText" placeholder="Nueva tarea..." />
<button type="submit">Añadir</button>
</form>
<TransitionGroup name="todo" tag="ul" class="todo-list">
<li
v-for="todo in sortedTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)" class="delete-btn">
×
</button>
</li>
</TransitionGroup>
</div>
</template>
<style>
.todo-list {
list-style: none;
padding: 0;
position: relative;
}
.todo-list li {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: white;
border: 1px solid #eee;
border-radius: 6px;
margin-bottom: 8px;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}
.todo-enter-active {
transition: all 0.3s ease-out;
}
.todo-leave-active {
transition: all 0.3s ease-in;
position: absolute;
width: 100%;
}
.todo-enter-from {
opacity: 0;
transform: translateY(-20px);
}
.todo-leave-to {
opacity: 0;
transform: translateX(40px);
}
.todo-move {
transition: transform 0.4s ease;
}
.delete-btn {
margin-left: auto;
background: none;
border: none;
color: #e74c3c;
font-size: 20px;
cursor: pointer;
}
</style>
Las tareas completadas se mueven suavemente al final de la lista gracias a la clase .todo-move. Los nuevos elementos se deslizan desde arriba y los eliminados se deslizan hacia la derecha, mientras que el resto de la lista se reorganiza con una animación de movimiento fluida.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje