VueJS ships with easy ways to integrate transitions between elements. Here, we are studying the particular case of lists and tables.
Without transitions, user experience is mediocre
In this article, we are going to work with a list of fruits displayed as a list and as a table. Indeed, setting up is slightly different in both cases.
This code, which will serve as a base for this article, does not show any transition. We can very well understand that a user may experience some difficulty in tracking what was changed every time the data is modified. I’m sure that the most curious of you went right into the code to see more clearly into it. Animated transitions are obviously very useful.
Moving elements inside a list
Animations I find the most spectacular is the ones that move elements inside a list or a table. And paradoxically, it’s the easiest to set up. In a simple table of data presentation, it can be enough to animate column sorting, or filtering for example.
VueJS provides us with a component named transition-group
. This component is specifically made to animate lists of elements. It then contains for sure a v-for
.
Transitions inside a list
The transition-group
component has a tag
attribute. This attribute allows us to fill out the type of tag that will actually be rendered in the DOM. Indeed, the transition-group
tag does not exist in HTML. It is specific to VueJS.
In our list example, the transition-group
component takes the place of the ul
tag. So we give the ul
value to the tag
attribute.
The transition-group
component also has a name
attribute. The value of this attribute will be used as a root to define animations in CSS.
<ul class="list-group">
<li class="list-group-item" v-for="item in items" :key="item.id">
[...]
</li>
</ul>
Then becomes:
<transition-group class="list-group" name="fruit-list" tag="ul">
<li class="list-group-item" v-for="item in items" :key="item.id">
[...]
</li>
</transition-group>
At this stage, the moving animation of elements in the list is operational. But we do not have specified any duration for this animation yet. So we cannot witness the transition animation. The duration is defined in CSS. And that is actually where the name
attribute of the transition-group
will be useful. VueJS will give the <name>-move
class to every moving element during the animation, where <name>
is the value of the name
attribute.
Here, for example, we wish that the move animation has a duration of 1 sec:
.fruit-list-move {
transition: transform 1s;
}
Transition in a table
Regarding tables, it should be the tbody
tag that would, in theory, be replaced by a transition-group
one. Yet if we do, the animation won’t work. That’s because of DOM template parsing that must absolutely find a tbody
tag inside a table
tag.
It is then mandatory to dynamically bind a transition-group
to the tbody
using the is
special attribute. What-the-hell-does-that-mean ? Nothing too complicated, see for yourself:
<table class="table">
[...]
<tbody>
<tr v-for="item in items" :key="item.id">
[...]
</tr>
</tbody>
</table>
Becomes:
<table class="table">
[...]
<tbody name="fruit-table" is="transition-group">
<tr v-for="item in items" :key="item.id">
[...]
</tr>
</tbody>
</table>
And as for the list, we need to add a rule in CSS:
.fruit-table-move {
transition: transform 1s;
}
Full result with VueJS
Here is how it goes:
Pretty cool, right! Even the addition of a new element is kinda animated, if we overlook the “downward jump” the tag that follows (Number of items) is experiencing. And that is exactly what we are going to solve in the next chapter of this article.
Adding an element to a list
To fix the “jump” issue we were just talking about, let’s see what happens in the current situation:
- The “Kiwis” element appears. It is actually fully present, just hidden behind the elements that follow it.
- The following elements are moved downwards. They are then moving, and take a full second to reach their new position.
We have the feeling that the “Kiwis” element appears gradually, but in fact it well arrives abruptly and is just gradually “un-hidden” by the elements of the list that follow it while they reach their final position. It’s this sudden appearing that is betrayed by the “jump” of the tag that follows our list.
To fix this unwanted behavior we are gonna have to affect the height of the element added to the list. As a matter of fact, we will modulate it’s height with an animation.
Enter transition
VueJS automatically applies classes to elements (tags) that appear or disappear from the DOM. These classes are dynamically applied at various stages of the enter transition:
- The
v-enter
class is briefly applied before the element is inserted. It gives the animation startup state. - The
v-enter-to
class is applied as soon as the element is inserted, as soon asv-enter
is removed. It represents the target state of the transition’s end, i.e. the state we want the element to reach at the end of the transition. - The
v-enter-active
class is applied during the entire transition. It is applied whenv-enter
is applied, and it is removed whenv-enter-to
is removed.


Juggling with this 3 classes will enable us to configure the enter transition animation of an element in a list or a table.
Appearing of an element in a list
In reality, the classes that are applied to our appearing element are not named v-enter
, v-enter-to
, and v-enter-active
. Those are suffixes. Indeed, the name of the transition classes is composed of the name given to the transition-group
element, followed by one of those names.
This way, for our list, the classes automatically attributed by VueJS will be fruit-list-enter
, fruit-list-enter-to
, and fruit-list-enter-active
.
In relation to the moving animations of the previous chapter, we have to make some adjustments. Indeed, our transitions do not concern only moves anymore, but also appearing. And also disappearing but we will learn about that in the next chapter.
We are then gonna replace:
.fruit-list-move {
transition: transform 1s;
}
By:
.fruit-list-item {
transition: all 1s;
}
And add the fruit-list-item
class to the elements of the list:
<transition-group class="list-group" name="fruit-list" tag="ul">
<li class="list-group-item fruit-list-item" v-for="item in items" :key="item.id">
[...]
</li>
</transition-group>
This way, every transition applied individually to the elements of the list will have a duration of 1 second. The moving animations are kept unmodified. And the entering and leaving transitions will also have a duration of 1 second.
Let’s now give the beginning and ending states of the appearing transition. We want the element to have a null height at the beginning of the animation, and grow to reach its final height at the end of the transition.
Yet we cannot know the height of a div in advance. It is computed based on the height of the elements it contains. On the other hand we can set a maximal height that we do not want it to exceed, thanks to the max-height
attribute. So for the animation end, we only need to decide of a maximal height that we know we won’t ever reach. But we need to stay quite close to the actual value because the animation speed will be calculated upon this target, and it could become too fast if this target is too far from the real value.
At the beginning of the animation, we want a height of 0px. We then set the max-height
attribute to 0, but this won’t be enough. Indeed, our div has some padding. It’s padding-top
and padding-bottom
attributes are not null by default. And they will prevent our div to fully disappear. The solution to this problem is pretty simple though, we only have to also set a value of 0 to those two attributes at the beginning of the animation (.fruit-list-enter
). It is not necessary to set a value for the end of the animation (.fruit-list-enter-to
) because as soon as the .fruit-list-enter
class is removed, the padding will have its default value as a target.
Here is the full CSS we need to write for the appearing animation of an element in a list:
.fruit-list-item {
transition: all 1s;
}
.fruit-list-enter {
max-height: 0px !important;
padding-top: 0px !important;
padding-bottom: 0px !important;
}
.fruit-list-enter-to {
max-height: 80px;
}
Notice the!important
instruction at the end of some lines. It is to make sure this rule will not be overwritten by any other.
Appearing of a line in a table
The same principle is applied to tables, except for one detail: we cannot force the height of a table’s line using its max-height
attribute.
As for lists, let’s start by adding a class to the lines of our table. This way:
.fruit-table-move {
transition: transform 1s;
}
Becomes:
.fruit-table-item {
transition: all 1s;
}
And we edit the tr
tag:
<table class="table mb-0">
[...]
<tbody name="fruit-table" is="transition-group">
<tr class="fruit-table-item" v-for="item in items" :key="item.id">
[...]
</tr>
</tbody>
</table>
So you got it: CSS for transitions in a table will be a bit different from CSS for transitions in a list. Indeed, the height of a tr
tag depends, like for a div, of its content. But unlike a div, it has no power over it. We are going to have to act on the lines content as well:
.fruit-table-item {
transition: all 1s;
}
.fruit-table-item > * {
transition: all 1s;
overflow: hidden;
}
.fruit-table-enter {
line-height: 0 !important;
}
.fruit-table-enter > * {
padding-top: 0px !important;
padding-bottom: 0px !important;
}
The height of a table’s lines is set by the line-height
attribute.
The > *
CSS selector, used twice in this code extract, enables us to select every child of the tr
tag. i.e. every tag it contains. We use this selector to set a padding-top
and padding-bottom
to 0 for every line of text.
And here is the result:
This example is quite simple because the lines of the table only contain text. If they contained buttons or pictures, setting up transitions with VueJS would be a bit more painful.
Removing an element from a list
We now reach the last step of this article’s subject: animation of an element disappearing from a list or a table.
As with appearing, VueJS automatically applies classes to elements leaving the DOM. Those classes are applied at different stages of the leave transition:
- The
v-leave
class is briefly applied (one frame) before the element starts the leave transition. It gives the animation startup state. - The
v-leave-to
class is applied as soon as the element starts leaving, as soon asv-leave
is removed. It represents the target state of the transition’s end, i.e. the state we want the element to reach at the end of the transition. - The
v-leave-active
class is applied during the entire transition. It is applied whenv-leave
is applied, and it is removed whenv-leave-to
is removed.


Disappearing of an element from a list or a table
Nothing magic here. The transition we want to build is the exact opposite of the appearing transition we just put into place. The line will shrink in height until it’s gone. Therefore the setup is exactly the same. We only need to add the proper classes to the CSS:
.fruit-list-item {
transition: all 1s;
}
.fruit-list-enter,
.fruit-list-leave-to {
max-height: 0px;
padding-top: 0px !important;
padding-bottom: 0px !important;
overflow: hidden;
}
.fruit-list-enter-to,
.fruit-list-leave {
max-height: 80px;
}
.fruit-table-item {
transition: all 1s;
}
.fruit-table-item > * {
transition: all 1s;
overflow: hidden;
}
.fruit-table-enter,
.fruit-table-leave-to {
line-height: 0;
}
.fruit-table-enter > *,
.fruit-table-leave-to > * {
padding-top: 0px !important;
padding-bottom: 0px !important;
}
And the final result looks like this:
There you go! You now know everything I know.
To go a little further
As I’m not making anything up, I’ll give up my sources. Or in this particular case: my source. The VueJS documentation, at the transitions chapter : https://vuejs.org/v2/guide/transitions.html
Do not hesitate to play around with the JSFiddle I provided in this article, our make your own ones. It’s the best way to understand and learn.