Efectos de Transición

Introducción

Vue ofrece una variedad de maneras para aplicar efectos de transición cuando los elementos son insertados, actualizados o eliminados del DOM. Esto incluye herramientas para:

En esta página, únicamente vamos a cubrir las transiciones de entrada, salida, y lista, pero puede ver la siguiente sección para controlar transiciones de estado.

Transiciones para Elementos/Componentes sencillos

Vue ofrece un componente de envoltura transition, que le permite añadir transiciones de entrada/salida para cualquier elemento o componente en los siguientes contextos:

Así es como se ve un ejemplo sencillo en acción:

<div id="demo">
<button v-on:click="show = !show">
Mostrar/Ocultar
</button>
<transition name="fade">
<p v-if="show">hola</p>
</transition>
</div>
new Vue({
el: '#demo',
data: {
show: true
}
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0
}

hola

Cuando un elemento envuelto en un componente transition es insertado o eliminado, ésto es lo que sucede:

  1. Vue determinará automáticamente si el elemento objetivo tiene transiciones o animaciones CSS aplicadas. Si las tiene, las clases de transición CSS serán añadidas/removidas en los momentos apropiados.

  2. Si el componente de transición ha ofrecido hooks de JavaScript, éstos serán invocados en los momentos apropiados.

  3. Si no son detectadas animaciones/transiciones CSS y no se han dado hooks de Javascript, las operaciones DOM para insertar y/o eliminar serán ejecutadas inmediatamente en el siguiente frame (Tenga en cuenta: hablamos de un frame de animación de navegador, es diferente al concepto de nextTick de Vue).

Clases de Transición

Existen cuatro clases aplicadas para transiciones de entrada/salida.

  1. v-enter: Estado inicial para entrada. Aplicada antes que el elemento sea insertado, se elimina después de un frame.
  2. v-enter-active: Estado activo y de finalización para entrada. Aplicada antes que el elemento sea insertado, se elimina cuando la animación/transición finaliza.
  3. v-leave: Estado inicial para salida. Aplicada cuando la transición de salida es activada, se elimina después de un frame.
  4. v-leave-active: Estado activo y de finalización para salida. Aplicada cuando la transición de salida es activada, se elimina cuando la animación/transición finaliza.

Diagrama de Transición

Cada una de estas clases usará un prefijo con el nombre de la transición. Aquí, el prefijo v- representa el prefijo por defecto cuando usa un elemento <transition> sin nombre. Si usa <transition name="mi-transicion"> por ejemplo, entonces la clase v-enter debe llamarse mi-transicion-enter.

v-enter-active y v-leave-active le da a usted la capacidad de especificar diferentes curvas de suavizado para transiciones de entrada/salida, lo cual podrá verlo en un ejemplo de la siguiente sección.

Transiciones CSS

Uno de los tipos de transición más comunes usa transiciones CSS. Aquí hay un ejemplo sencillo:

<div id="example-1">
<button @click="show = !show">
Mostrar/Ocultar
</button>
<transition name="slide-fade">
<p v-if="show">hola</p>
</transition>
</div>
new Vue({
el: '#example-1',
data: {
show: true
}
})
/* Las animaciones de entrada y salida pueden usar */
/* funciones de espera y duración diferentes. */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}

hola

Animaciones CSS

Las animaciones CSS son aplicadas de la misma forma que las transiciones CSS, la diferencia es que v-enter no es eliminado inmediatamente después que el elemento sea insertado, sino que se elimina en el evento animationend.

Aquí hay un ejemplo, omitiendo las reglas con prefijo de CSS para mayor brevedad.

<div id="example-2">
<button @click="show = !show">Mostrar/Ocultar</button>
<transition name="bounce">
<p v-if="show">Mírame!</p>
</transition>
</div>
new Vue({
el: '#example-2',
data: {
show: true
}
})
.bounce-enter-active {
animation: bounce-in .5s;
}
.bounce-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}

Mírame!

Clases de Transición Personalizadas

Usted también puede especificar clases de transición personalizadas cuando usa los siguientes atributos:

Estas van a sobreescribir los nombres convencionales de las clases. Esto es especialmente útil cuando desea combinar el sistema de transición de Vue con una librería de animaciones CSS existente, como Animate.css.

Por ejemplo:

<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">

<div id="example-3">
<button @click="show = !show">
Mostrar/Ocultar
</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hola</p>
</transition>
</div>
new Vue({
el: '#example-3',
data: {
show: true
}
})

hola

Usando Animaciones y Trancisiones Juntas

Vue necesita agregar listeners de eventos para poder saber cuándo una transición ha finalizado. Puede ser mediante transitionended o animationended, dependiendo del tipo de reglas CSS aplicadas. Si únicamente está usando la una o la otra, Vue detecta automáticamente el tipo correcto.

Sin embargo, en algunos casos usted desea tener ambos tipos en el mismo elemento, por ejemplo tener una animación CSS activada por Vue, junto a un efecto de transición en el hover. En estos casos, debe declarar explícitamente el tipo que quiera que Vue use, usando el atributo type, con un valor ya sea de animation o transition.

Hooks de JavaScript

Usted también puede definir hooks de JavaScript en los atributos:

<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:enter-cancelled="enterCancelled"

v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
>
<!-- ... -->
</transition>
// ...
methods: {
// --------
// ENTRADA
// --------

beforeEnter: function (el) {
// ...
},
// el callback done es opcional cuando
// es usado junto a CSS
enter: function (el, done) {
// ...
done()
},
afterEnter: function (el) {
// ...
},
enterCancelled: function (el) {
// ...
},

// --------
// SALIDA
// --------

beforeLeave: function (el) {
// ...
},
// el callback done es opcional cuando
// es usado junto a CSS
leave: function (el, done) {
// ...
done()
},
afterLeave: function (el) {
// ...
},
// leaveCancelled sólo es permitido dentro de un v-show
leaveCancelled: function (el) {
// ...
}
}

Estos hooks pueden ser usados junto a transiciones/animaciones CSS o por cuenta propia.

Cuando use transiciones JavaScript, los callbacks done son requeridos para los hooks enter y leave. De otra forma, serán llamados síncronamente y la transición finalizará imediatamente.

También es buena idea añadir explícitamente v-bind:css="false" para las transiciones JavaScript, de modo que Vue pueda obviar la detección de CSS. Esto también previene que algunas reglas CSS accidentalmente interfieran en la transición.

Ahora veamos un ejemplo. Aquí hay una transición sencilla en JavaScript usando Velocity.js:

<!--
Velocity funciona bastante parecido a jQuery.animate
y es una gran opción para animaciones JavaScript
-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

<div id="example-4">
<button @click="show = !show">
Mostrar/Ocultar
</button>
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:leave="leave"
v-bind:css="false"
>
<p v-if="show">
Demo
</p>
</transition>
</div>
new Vue({
el: '#example-4',
data: {
show: false
},
methods: {
beforeEnter: function (el) {
el.style.opacity = 0
},
enter: function (el, done) {
Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
Velocity(el, { fontSize: '1em' }, { complete: done })
},
leave: function (el, done) {
Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 })
Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
Velocity(el, {
rotateZ: '45deg',
translateY: '30px',
translateX: '30px',
opacity: 0
}, { complete: done })
}
}
})

Demo

Transiciones en Renderizado Inicial

Si usted quisiera también aplicar una transición en la renderización inicial de un nodo, puede añadir el atributo appear:

<transition appear>
<!-- ... -->
</transition>

Por defecto, esto usará la transición especificada para entrada y salida. Sin embargo, si lo quisiera, puede también especificar clases CSS personalizadas:

<transition
appear
appear-class="custom-appear-class"
appear-to-class="custom-appear-to-class" (2.1.8+)
appear-active-class="custom-appear-active-class"
>
<!-- ... -->
</transition>

y hooks de JavaScript personalizados:

<transition
appear
v-on:before-appear="customBeforeAppearHook"
v-on:appear="customAppearHook"
v-on:after-appear="customAfterAppearHook"
v-on:appear-cancelled="customAppearCancelledHook"
>
<!-- ... -->
</transition>

Transiciones Entre Elementos

Ya discutiremos las transiciones entre componentes más adelante, pero también puede aplicar transiciones entre elementos básicos usando v-if/v-else. Una de las transiciones entre dos elementos más comunes es la que se usa entre un contenedor de lista, y un mensaje describiendo una lista vacía:

<transition>
<table v-if="items.length > 0">
<!-- ... -->
</table>
<p v-else>No se han encontrado elementos.</p>
</transition>

Funciona bien, pero hay un inconveniente que debemos tener en cuenta:

Cuando intercambia elementos que tienen el mismo nombre de etiqueta, debe decirle a Vue que ellos son elementos diferentes usando atributos key únicos. De otra forma, el compilador de Vue únicamente reemplazará el contenido del elemento para mejorar la eficiencia. Aún cuando sea innecesario, es considerado buena práctica siempre usar key en múltiples elementos dentro de un componente <transition>.

Por ejemplo:

<transition>
<button v-if="isEditing" key="save">
Guardar
</button>
<button v-else key="edit">
Editar
</button>
</transition>

En estos casos, puede también usar el atributo key para realizar una trancisión entre estados diferentes del mismo elemento. En vez de usar v-if y v-else, el ejemplo anterior puede ser re-escrito como:

<transition>
<button v-bind:key="isEditing">
{{ isEditing ? 'Guardar' : 'Editar' }}
</button>
</transition>

Es posible realizar una transición entre cualquier número de elementos, ya sea usando muchos v-if, o asignando una propiedad dinámica a un elemento. Por ejemplo:

<transition>
<button v-if="docState === 'saved'" key="saved">
Editar
</button>
<button v-if="docState === 'edited'" key="edited">
Guardar
</button>
<button v-if="docState === 'editing'" key="editing">
Cancelar
</button>
</transition>

Que también puede ser escrito de la siguiente forma:

<transition>
<button v-bind:key="docState">
{{ buttonMessage }}
</button>
</transition>
// ...
computed: {
buttonMessage: function () {
switch (this.docState) {
case 'saved': return 'Editar'
case 'edited': return 'Guardar'
case 'editing': return 'Cancelar'
}
}
}

Modos de Transición

Aún se presenta un problema. Intente hacer click sobre el botón:

Mientras se hace la transición entre el botón “on” y el botón “off”, ambos botones son renderizados - uno realizando la transición de salida mientras el otro realiza la de entrada. Este comportamiento ocurre por defecto en <transition> - las entradas y salidas suceden simultáneamente.

Algunas veces funciona genial, por ejemplo cuando estamos realizando transiciones entre elementos ubicados absolutamente unos encima de otros:

O también cuando se trasladan para que parezcan transiciones de deslizamiento:

Sin embargo, transiciones de entrada y salida simultáneas no siempre son deseables, de modo que Vue ofrece nodos de transición alternativos:

Ahora actualicemos las transiciones para nuestros botones on/off con out-in:

<transition name="fade" mode="out-in">
<!-- ... los botones ... -->
</transition>

Con sólo añadir un sencillo atributo, hemos arreglado la transición original sin tener que agregar estilos especiales.

El modo in-out no es usado a menudo, pero a veces puede ser útil para un efecto de transición ligeramente diferente. Intentemos combinarlo con la transición slide-fade con la que trabajamos anteriormente:

Se vé genial, ¿no?

Transiciones Entre Componentes

Las transiciones entre componentes son aún más sencillas - ni siquiera necesitamos el atributo key. En su lugar, sólo envolvemos un componente dinámico:

<transition name="component-fade" mode="out-in">
<component v-bind:is="view"></component>
</transition>
new Vue({
el: '#transition-components-demo',
data: {
view: 'v-a'
},
components: {
'v-a': {
template: '<div>Componente A</div>'
},
'v-b': {
template: '<div>Componente B</div>'
}
}
})
.component-fade-enter-active, .component-fade-leave-active {
transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to
/* .component-fade-leave-active below version 2.1.8 */ {
opacity: 0;
}

Transiciones de Listas

Hasta ahora, hemos logrado realizar transiciones para:

¿Qué tenemos para el caso en que tengamos una lista completa de elementos que queramos renderizar simultáneamente, por ejemplo con v-for? En ese caso, usaremos el componente <transition-group>. Pero antes que lo veamos en un ejemplo, hay algunas cosas que es importante conocer sobre éste componente.

Transiciones de Entrada/Salida en Listas

Ahora veamos un ejemplo sencillo, transiciones de entrada y salida usando las mismas clases CSS que hemos usado anteriormente:

<div id="list-demo">
<button v-on:click="add">Agregar</button>
<button v-on:click="remove">Remover</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
new Vue({
el: '#list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10
},
methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.items.length)
},
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++)
},
remove: function () {
this.items.splice(this.randomIndex(), 1)
},
}
})
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
{{ item }}

Hay un problema presente en este ejemplo. Cuando agrega o remueve un elemento, los que están alrededor saltan instantáneamente a su nueva ubicación en vez de realizar una transición suave. Eso lo arreglaremos más adelante.

Transiciones de Movimiento en Listas

El componente <transition-group> tiene otro truco bajo su manga. No sólo puede animar las entradas y salidas, sino también cambios en la posición. El único nuevo concepto que debe conocer para usar esta característica es la adición de la clase v-move, la cual es añadida cuando los elementos están cambiando de posición. Como con las otras clases, su prefijo será el mismo que el valor que le haya asignado al atributo name, y también puede especificar una clase CSS con el atributo move-class.

Esta clase es muy útil para especificar el tiempo y la curva de suavizado de la transición, como verá a continuación:

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

<div id="flip-list-demo" class="demo">
<button v-on:click="shuffle">Mezclar</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" v-bind:key="item">
{{ item }}
</li>
</transition-group>
</div>
new Vue({
el: '#flip-list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9]
},
methods: {
shuffle: function () {
this.items = _.shuffle(this.items)
}
}
})
.flip-list-move {
transition: transform 1s;
}
  • {{ item }}
  • Esto podrá parecer magia, pero por debajo, Vue está usando una sencilla técnica de animación llamada FLIP para realizar las transiciones de los elementos suavemente de su antigua posición a la nueva usando transformaciones.

    ¡Podemos combinar esta técnica con nuestra implementación anterior para animar cualquier cambio posible en nuestra lista!

    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

    <div id="list-complete-demo" class="demo">
    <button v-on:click="shuffle">Mezclar</button>
    <button v-on:click="add">Agregar</button>
    <button v-on:click="remove">Remover</button>
    <transition-group name="list-complete" tag="p">
    <span
    v-for="item in items"
    v-bind:key="item"
    class="list-complete-item"
    >
    {{ item }}
    </span>
    </transition-group>
    </div>
    new Vue({
    el: '#list-complete-demo',
    data: {
    items: [1,2,3,4,5,6,7,8,9],
    nextNum: 10
    },
    methods: {
    randomIndex: function () {
    return Math.floor(Math.random() * this.items.length)
    },
    add: function () {
    this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove: function () {
    this.items.splice(this.randomIndex(), 1)
    },
    shuffle: function () {
    this.items = _.shuffle(this.items)
    }
    }
    })
    .list-complete-item {
    transition: all 1s;
    display: inline-block;
    margin-right: 10px;
    }
    .list-complete-enter, .list-complete-leave-to
    /* .list-complete-leave-active below version 2.1.8 */ {
    opacity: 0;
    transform: translateY(30px);
    }
    .list-complete-leave-active {
    position: absolute;
    }
    {{ item }}

    Algo importante para tener en cuenta es que éstas transiciones FLIP no funcionan en elementos con display: inline. Como alternativa, puede usar display: inline-block o ubicar los elementos en un contexto flexible.

    Estas animaciones FLIP no están limitadas a un eje particular. Los elementos en una grilla multidimensional pueden realizar transiciones igual de fácil:

    Sudoku Perezoso

    Continúa apretando el botón mezclar hasta que ganes.

    {{ cell.number }}

    Retardando Transiciones de Lista

    Es posible retardar transiciones en una lista usando atributos de datos para comunicarse con transciones JavaScript:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

    <div id="staggered-list-demo">
    <input v-model="query">
    <transition-group
    name="staggered-fade"
    tag="ul"
    v-bind:css="false"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
    >
    <li
    v-for="(item, index) in computedList"
    v-bind:key="item.msg"
    v-bind:data-index="index"
    >{{ item.msg }}</li>
    </transition-group>
    </div>
    new Vue({
    el: '#staggered-list-demo',
    data: {
    query: '',
    list: [
    { msg: 'Bruce Lee' },
    { msg: 'Jackie Chan' },
    { msg: 'Chuck Norris' },
    { msg: 'Jet Li' },
    { msg: 'Kung Fury' }
    ]
    },
    computed: {
    computedList: function () {
    var vm = this
    return this.list.filter(function (item) {
    return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
    })
    }
    },
    methods: {
    beforeEnter: function (el) {
    el.style.opacity = 0
    el.style.height = 0
    },
    enter: function (el, done) {
    var delay = el.dataset.index * 150
    setTimeout(function () {
    Velocity(
    el,
    { opacity: 1, height: '1.6em' },
    { complete: done }
    )
    }, delay)
    },
    leave: function (el, done) {
    var delay = el.dataset.index * 150
    setTimeout(function () {
    Velocity(
    el,
    { opacity: 0, height: 0 },
    { complete: done }
    )
    }, delay)
    }
    }
    })
  • {{ item.msg }}
  • Transiciones Reusables

    Las transiciones pueden ser reusadas a través del sistema de componentes de Vue. Para crear una transición reusable, todo lo que debe hacer es ubicar un componente <transition> o <transition-group> en la raíz, luego enviar cualquier hijo en el componente de transición.

    Aquí hay un ejemplo usando un componente de plantilla:

    Vue.component('my-special-transition', {
    template: '\
    <transition\
    name="very-special-transition"\
    mode="out-in"\
    v-on:before-enter="beforeEnter"\
    v-on:after-enter="afterEnter"\
    >\
    <slot></slot>\
    </transition>\
    ',
    methods: {
    beforeEnter: function (el) {
    // ...
    },
    afterEnter: function (el) {
    // ...
    }
    }
    })

    Y los componentes funcionales son especialmente efectivos para ésta tarea:

    Vue.component('my-special-transition', {
    functional: true,
    render: function (createElement, context) {
    var data = {
    props: {
    name: 'very-special-transition',
    mode: 'out-in'
    },
    on: {
    beforeEnter: function (el) {
    // ...
    },
    afterEnter: function (el) {
    // ...
    }
    }
    }
    return createElement('transition', data, context.children)
    }
    })

    Transiciones Dinámicas

    Sí, ¡incluso las transiciones en Vue son controladas por datos! El ejemplo más básico de una transición dinámica es asignar el atributo name a una propiedad dinámica.

    <transition v-bind:name="transitionName">
    <!-- ... -->
    </transition>

    Esto puede ser útil cuando ha definido animaciones/transiciones CSS usando las convenciones de clases de transición de Vue y simplemente quiere cambiar entre ellas.

    Pero en realidad, cualquier transición puede ser asignada dinámicamente. Y no son sólo atributos. Ya que los hooks de eventos son métodos, tienen acceso a cualquier dato del contexto. Esto significa que, dependiendo del estado de su componente, sus transiciones pueden comportarse de forma diferente.

    <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

    <div id="dynamic-fade-demo" class="demo">
    Transición de Entrada: <input type="range" v-model="fadeInDuration" min="0" v-bind:max="maxFadeDuration">
    Transición de Salida: <input type="range" v-model="fadeOutDuration" min="0" v-bind:max="maxFadeDuration">
    <transition
    v-bind:css="false"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
    >
    <p v-if="show">hola</p>
    </transition>
    <button
    v-if="stop"
    v-on:click="stop = false; show = false"
    >Comenzar Animación</button>
    <button
    v-else
    v-on:click="stop = true"
    >Parar!</button>
    </div>
    new Vue({
    el: '#dynamic-fade-demo',
    data: {
    show: true,
    fadeInDuration: 1000,
    fadeOutDuration: 1000,
    maxFadeDuration: 1500,
    stop: true
    },
    mounted: function () {
    this.show = false
    },
    methods: {
    beforeEnter: function (el) {
    el.style.opacity = 0
    },
    enter: function (el, done) {
    var vm = this
    Velocity(el,
    { opacity: 1 },
    {
    duration: this.fadeInDuration,
    complete: function () {
    done()
    if (!vm.stop) vm.show = false
    }
    }
    )
    },
    leave: function (el, done) {
    var vm = this
    Velocity(el,
    { opacity: 0 },
    {
    duration: this.fadeOutDuration,
    complete: function () {
    done()
    vm.show = true
    }
    }
    )
    }
    }
    })
    Transición de Entrada: Transición de Salida:

    hola

    Finalmente, la mejor forma de crear transiciones dinámicas es a través de componentes que aceptan props para cambiar la naturaleza de la transición a ser usada. Puede sonar como cliché, pero el único límite realmente es su imaginación.