Animation Techniques for Adding and Removing Items From a Stack
Animating elements with CSS can either be quite easy or quite difficult depending on what you are trying to do. Changing the background color of a button when you hover over it? Easy. Animating the position and size of an element in a performant way that also affects the position of other elements? Tricky! That’s exactly what we’ll get into here in this article.
A common example is removing an item from a stack of items. The items stacked on top need to fall downwards to account for the space of an item removed from the bottom of the stack. That is how things behave in real life, and users may expect this kind of life-like motion on a website. When it doesn’t happen, it’s possible the user is confused or momentarily disorientated. You expect something to behave one way based on life experience and get something completely different, and users may need extra time to process the unrealistic movement.
Here is a demonstration of a UI for adding items (click the button) or removing items (click the item).
You could paper over the poor UI slightly by adding a “fade out” animation or something, but the result won’t be that great, as the list will will abruptly collapse and cause those same cognitive issues.
Applying CSS-only animations to a dynamic DOM event (adding brand new elements and fully removing elements) is extremely tricky work. We’re going to face this problem head-on and go over three very different types of animations that handle this, all accomplishing the same goal of helping users understand changes to a list of items. By the time we’re done, you’ll be armed to use these animations, or build your own based on the concepts.
We will also touch upon accessibility and how elaborate HTML layouts can still retain some compatibility with accessibility devices with the help of ARIA attributes.
The Slide-Down Opacity Animation
A very modern approach (and my personal favorite) is when newly-added elements fade-and-float into position vertically depending on where they are going to end up. This also means the list needs to “open up” a spot (also animated) to make room for it. If an element is leaving the list, the spot it took up needs to contract.
Because we have so many different things going on at the same time, we need to change our DOM structure to wrap each .list-item
in a container class appropriately titled .list-container
. This is absolutely essential in order to get our animation to work.
<ul class="list">
<li class="list-container">
<div class="list-item">List Item</div>
</li>
<li class="list-container">
<div class="list-item">List Item</div>
</li>
<li class="list-container">
<div class="list-item">List Item</div>
</li>
<li class="list-container">
<div class="list-item">List Item</div>
</li>
</ul>
<button class="add-btn">Add New Item</button>
Now, the styling for this is unorthodox because, in order to get our animation effect to work later on, we need to style our list in a very specific way that gets the job done at the expense of sacrificing some customary CSS practices.
.list {
list-style: none;
}
.list-container {
cursor: pointer;
font-size: 3.5rem;
height: 0;
list-style: none;
position: relative;
text-align: center;
width: 300px;
}
.list-container:not(:first-child) {
margin-top: 10px;
}
.list-container .list-item {
background-color: #D3D3D3;
left: 0;
padding: 2rem 0;
position: absolute;
top: 0;
transition: all 0.6s ease-out;
width: 100%;
}
.add-btn {
background-color: transparent;
border: 1px solid black;
cursor: pointer;
font-size: 2.5rem;
margin-top: 10px;
padding: 2rem 0;
text-align: center;
width: 300px;
}
How to handle spacing
First, we’re using margin-top
to create vertical space between the elements in the stack. There’s no margin on the bottom so that the other list items can fill the gap created by removing a list item. That way, it still has margin on the bottom even though we have set the container height to zero. That extra space is created between the list item that used to be directly below the deleted list item. And that same list item should move up in reaction to the deleted list item’s container having zero height. And because this extra space expands the vertical gap between the list items further then we want it to. So that’s why we use margin-top
— to prevent that from happening.
But we only do this if the item container in question isn’t the first one in the list. That’s we used :not(:first-child)
— it targets all of the containers except the very first one (an enabling selector). We do this because we don’t want the very first list item to be pushed down from the top edge of the list. We only want this to happen to every subsequent item thereafter instead because they are positioned directly below another list item whereas the first one isn’t.
Now, this is unlikely to make complete sense because we are not setting any elements to zero height at the moment. But we will later on, and in order to get the vertical spacing between the list elements correct, we need to set the margin like we do.
A note about positioning
Something else that is worth pointing out is the fact that the .list-item
elements nested inside of the parent .list-container
elements are set to have a position
of absolute
, meaning that they are positioned outside of the DOM and in relation to their relatively-positioned .list-container
elements. We do this so that we can get the .list-item
element to float upwards when removed, and at the same time, get the other .list-item
elements to move and fill the gap that removing this .list-item
element has left. When this happens, the .list-container
element, which isn’t positioned absolute
and is therefore affected by the DOM, collapses its height allowing the other .list-container
elements to fill its place, and the .list-item
element — which is positioned with absolute
— floats upwards, but doesn’t affect the structure of the list as it isn’t affected by the DOM.
Handling height
Unfortunately, we haven’t yet done enough to get a proper list where the individual list-items are stacked one by one on top of each other. Instead, all we will be able to see at the moment is just a single .list-item
that represents all of the list items piled on top of each other in the exact same place. That’s because, although the .list-item
elements may have some height via their padding
property, their parent elements do not, but have a height of zero instead. This means that we don’t have anything in the DOM that is actually separating these elements out from each other because in order to do that, we would need our .list-item
containers to have some height because, unlike their child element, they are affected by the DOM.
To get the height of our list containers to perfectly match the height of their child elements, we need to use JavaScript. So, we store all of our list items within a variable. Then, we create a function that is called immediately as soon as the script is loaded.
This becomes the function that handles the height of the list container elements:
const listItems = document.querySelectorAll('.list-item');
function calculateHeightOfListContainer(){
};
calculateHeightOfListContainer();
The first thing that we do is extract the very first .list-item
element from the list. We can do this because they are all the same size, so it doesn’t matter which one we use. Once we have access to it, we store its height, in pixels, via the element’s clientHeight
property. After this, we create a new element that is prepended to the document’s
body
immediately after so that we can directly create a CSS class that incorporates the height value we just extracted. And with this element safely in the DOM, we write a new
.list-container
class with styles that automatically have priority over the styles declared in the external stylesheet since these styles come from an actual tag. That gives the
.list-container
classes the same height as their .list-item
children.
const listItems = document.querySelectorAll('.list-item');
function calculateHeightOfListContainer() {
const firstListItem = listItems[0];
let heightOfListItem = firstListItem.clientHeight;
const styleTag = document.createElement('style');
document.body.prepend(styleTag);
styleTag.innerHTML = `.list-container{
height: ${heightOfListItem}px;
}`;
};
calculateHeightOfListContainer();
Showing and Hiding
Right now, our list looks a little drab — the same as the what we saw in the first example, just without any of the addition or removal logic, and styled in a completely different way to the list constructed from
- and
tags list that were used in that opening example.
We’re going to do something now that may seem inexplicable at the moment and modify our .list-container
and .list-item
classes. We’re also creating extra styling for both of these classes that will only be added to them if a new class, .show
, is used in conjunction with both of these classes separately.
The purpose we’re doing this is to create two states for both the .list-container
and the .list-item
elements. One state is without the .show
classes on both of these elements, and this state represents the elements as they are animated out from the list. The other state contains the .show
class added to both of these elements. It represents the specified .list-item
as firmly instantiated and visible in the list.
In just a bit, we will switch between these two states by adding/removing the .show
class from both the parent and the container of a specific .list-item
. We’ll combined that with a CSS transition
between these two states.
Notice that combining the .list-item
class with the .show
class introduces some extra styles to things. Specifically, we’re introducing the animation that we are creating where the list item fades downwards and into visibility when it is added to the list — the opposite happens when it is removed. Since the most performant way to animate elements positions is with the transform
property, that is what we will use here, applying opacity
along the way to handle the visibility part. Because we already applied a transition
property on both the .list-item
and the .list-container
elements, a transition automatically takes place whenever we add or remove the .show
class to both of these elements due to the extra properties that the .show
class brings, causing a transition whenever we either add or remove these new properties.
.list-container {
cursor: pointer;
font-size: 3.5rem;
height: 0;
list-style: none;
position: relative;
text-align: center;
width: 300px;
}
.list-container.show:not(:first-child) {
margin-top: 10px;
}
.list-container .list-item {
background-color: #D3D3D3;
left: 0;
opacity: 0;
padding: 2rem 0;
position: absolute;
top: 0;
transform: translateY(-300px);
transition: all 0.6s ease-out;
width: 100%;
}
.list-container .list-item.show {
opacity: 1;
transform: translateY(0);
}
In response to the .show
class, we are going back to our JavaScript file and changing our only function so that the .list-container
element are only given a height
property if the element in question also has a .show
class on it as well, Plus, we are applying a transition
property to our standard .list-container
elements, and we will do it in a setTimeout
function. If we didn’t, then our containers would animate on the initial page load when the script is loaded, and the heights are applied the first time, which isn’t something we want to happen.
const listItems = document.querySelectorAll('.list-item');
function calculateHeightOfListContainer(){
const firstListItem = listItems[0];
let heightOfListItem = firstListItem.clientHeight;
const styleTag = document.createElement('style');
document.body.prepend(styleTag);
styleTag.innerHTML = `.list-container.show {
height: ${heightOfListItem}px;
}`;
setTimeout(function() {
styleTag.innerHTML += `.list-container {
transition: all 0.6s ease-out;
}`;
}, 0);
};
calculateHeightOfListContainer();
Now, if we go back and view the markup in DevTools, then we should be able to see that the list has disappeared and all that is left is the button. The list hasn’t disappeared because these elements have been removed from the DOM; it has disappeared because of the .show
class which is now a required class that must be added to both the .list-item
and the .list-container
elements in order for us to be able to view them.
The way to get the list back is very simple. We add the .show
class to all of our .list-container
elements as well as the .list-item
elements contained inside. And once this is done we should be able to see our pre-created list items back in their usual place.
<ul class="list">
<li class="list-container show">
<div class="list-item show">List Item</div>
</li>
<li class="list-container show">
<div class="list-item show">List Item</div>
</li>
<li class="list-container show">
<div class="list-item show">List Item</div>
</li>
<li class="list-container show">
<div class="list-item show">List Item</div>
</li>
</ul>
<button class="add-btn">Add New Item</button>
We won’t be able to interact with anything yet though because to do that — we need to add more to our JavaScript file.
The first thing that we will do after our initial function is declare references to both the button that we click to add a new list item, and the .list
element itself, which is the element that wraps around every single .list-item
and its container. Then we select every single .list-container
element nested inside of the parent .list
element and loop through them all with the forEach
method. We assign a method in this callback, removeListItem
, to the onclick
event handler of each .list-container
. By the end of the loop, every single .list-container
instantiated to the DOM on a new page load calls this same method whenever they are clicked.
Once this is done, we assign a method to the onclick
event handler for addBtn
so that we can activate code when we click on it. But obviously, we won’t create that code just yet. For now, we are merely logging something to the console for testing.
const addBtn = document.querySelector('.add-btn');
const list = document.querySelector('.list');
function removeListItem(e){
console.log('Deleted!');
}
// DOCUMENT LOAD
document.querySelectorAll('.list .list-container').forEach(function(container) {
container.onclick = removeListItem;
});
addBtn.onclick = function(e){
console.log('Add Btn');
}
Starting work on the onclick
event handler for addBtn
, the first thing that we want to do is create two new elements: container
and listItem
. Both elements represent the .list-item
element and their respective .list-container
element, which is why we assign those exact classes to them as soon as we create the them.
Once these two elements are prepared, we use the append
method on the container
to insert the listItem
inside of it as a child, the same as how these elements that are already in the list are formatted. With the listItem
successfully appended as a child to the container
, we can move the container
element along with its child listItem
element to the DOM with the insertBefore
method. We do this because we want new items to appear at the bottom of the list but before the addBtn
, which needs to stay at the very bottom of the list. So, by using the parentNode
attribute of addBtn
to target its parent, list
, we are saying that we want to insert the element as a child of list
, and the child that we are inserting (container
) will be inserted before the child that is already on the DOM and that we have targeted with the second argument of the insertBefore
method, addBtn
.
Finally, with the .list-item
and its container successfully added to the DOM, we can set the container’s onclick
event handler to match the same method as every other .list-item
already on the DOM by default.
addBtn.onclick = function(e){
const container = document.createElement('li');
container.classList.add('list-container');
const listItem = document.createElement('div');
listItem.classList.add('list-item');
listItem.innerHTML = 'List Item';
container.append(listItem);
addBtn.parentNode.insertBefore(container, addBtn);
container.onclick = removeListItem;
}
If we try this out, then we won’t be able to see any changes to our list no matter how many times we click the addBtn
. This isn’t an error with the click
event handler. Things are working exactly how they should be. The .list-item
elements (and their containers) are added to the list in the correct place, it is just that they are getting added without the .show
class. As a result, they don’t have any height to them, which is why we can’t see them and is why it looks like nothing is happening to the list.
To get each newly added .list-item
to animate into the list whenever we click on the addBtn
, we need to apply the .show
class to both the .list-item
and its container, just as we had to do to view the list items already hard-coded into the DOM.
The problem is that we cannot just add the .show
class to these elements instantly. If we did, the new .list-item
statically pops into existence at the bottom of the list without any animation. We need to register a few styles before the animation additional styles that override those initial styles for an element to know what transition
to make. Meaning, that if we just apply the .show
class to are already in place — so no transition.
The solution is to apply the .show
classes in a setTimeout
callback, delaying the activation of the callback by 15 milliseconds, or 1.5/100th of a second. This imperceptible delay is long enough to create a transition
from the proviso state to the new state that is created by adding the .show
class. But that delay is also short enough that we will never know that there was a delay in the first place.
addBtn.onclick = function(e){
const container = document.createElement('li');
container.classList.add('list-container');
const listItem = document.createElement('div');
listItem.classList.add('list-item');
listItem.innerHTML = 'List Item';
container.append(listItem);
addBtn.parentNode.insertBefore(container, addBtn);
container.onclick = removeListItem;
setTimeout(function(){
container.classList.add('show');
listItem.classList.add('show');
}, 15);
}
Success! It is now time to handle how we remove list items when they are clicked.
Removing list items shouldn’t be too hard now because we have already gone through the difficult task of adding them. First, we need to make sure that the element we are dealing with is the .list-container
element instead of the .list-item
element. Due to event propagation, it is likely that the target that triggered this click event was the .list-item
element.
Since we want to deal with the associated .list-container
element instead of the actual .list-item
element that triggered the event, we’re using a while-loop to loop one ancestor upwards until the element held in container
is the .list-container
element. We know it works when container
gets the .list-container
class, which is something that we can discover by using the contains
method on the classList
property of the container
element.
Once we have access to the container
, we promptly remove the .show
class from both the container
and its .list-item
once we have access to that as well.
function removeListItem(e) {
let container = e.target;
while (!container.classList.contains('list-container')) {
container = container.parentElement;
}
container.classList.remove('show');
const listItem = container.querySelector('.list-item');
listItem.classList.remove('show');
}
And here is the finished result:
Accessibility & Performance
Now you may be tempted to just leave the project here because both list additions and removals should now be working. But it is important to keep in mind that this functionality is only surface level and there are definitely some touch ups that need to be made in order to make this a complete package.
First of all, just because the removed elements have faded upwards and out of existence and the list has contracted to fill the gap that it has left behind does not mean that the removed element has been removed from the DOM. In fact, it hasn’t. Which is a performance liability because it means that we have elements in the DOM that serve no purpose other than to just accumulate in the background and slow down our application.
To solve this, we use the ontransitionend
method on the container element to remove it from the DOM but only when the transition caused by us removing the .show
class has finished so that its removal couldn’t possibly interrupt our transition.
function removeListItem(e) {
let container = e.target;
while (!container.classList.contains('list-container')) {
container = container.parentElement;
}
container.classList.remove('show');
const listItem = container.querySelector('.list-item');
listItem.classList.remove('show');
container.ontransitionend = function(){
container.remove();
}
}
We shouldn’t be able to see any difference at this point because allwe did was improve the performance — no styling updates.
The other difference is also unnoticeable, but super important: compatibility. Because we have used the correct
- and
tags, devices should have no problem with correctly interpreting what we have created as an unordered list.
Other considerations for this technique
A problem that we do have however, is that devices may have a problem with the dynamic nature of our list, like how the list can change its size and the number of items that it holds. A new list item will be completely ignored and removed list items will be read as if they still exist.
So, in order to get devices to re-interpret our list whenever the size of it changes, we need to use ARIA attributes. They help get our nonstandard HTML list to be recognized as such by compatibility devices. That said, they are not a guaranteed solution here because they are never as good for compatibility as a native tag. Take the
- tag as an example — no need to worry about that because we were able to use the native unordered list element.
We can use the aria-live
attribute to the .list
element. Everything nested inside of a section of the DOM marked with aria-live
becomes responsive. In other words, changes made to an element with aria-live
is recognized, allowing them to issue an updated response. In our case, we want things highly reactive and we do that be setting the aria live
attribute to assertive
. That way, whenever a change is detected, it will do so, interrupting whatever task it was currently doing at the time to immediately comment on the change that was made.
<ul class="list" role="list" aria-live="assertive">
The Collapse Animation
This is a more subtle animation where, instead of list items floating either up or down while changing opacity, elements instead just collapse or expand outwards as they gradually fade in or out; meanwhile, the rest of the list repositions itself to the transition taking place.
The cool thing about the list (and perhaps some remission for the verbose DOM structure we created), would be the fact that we can change the animation very easily without interfering with the main effect.
So, to achieve this effect, we start of by hiding overflow
on our .list-container
. We do this so that when the .list-container
collapses in on itself, it does so without the child .list-item
flowing beyond the list container’s boundaries as it shrinks. Apart from that, the only other thing that we need to do is remove the transform
property from the .list-item
with the .show
class since we don’t want the .list-item
to float upwards anymore.
.list-container {
cursor: pointer;
font-size: 3.5rem;
height: 0;
overflow: hidden;
list-style: none;
position: relative;
text-align: center;
width: 300px;
}
.list-container.show:not(:first-child) {
margin-top: 10px;
}
.list-container .list-item {
background-color: #D3D3D3;
left: 0;
opacity: 0;
padding: 2rem 0;
position: absolute;
top: 0;
transition: all 0.6s ease-out;
width: 100%;
}
.list-container .list-item.show {
opacity: 1;
}
The Side-Slide Animation
This last animation technique is strikingly different fromithe others in that the container
animation and the .list-item
animation are actually out of sync. The .list-item
is sliding to the right when it is removed from the list, and sliding in from the right when it is added to the list. There needs to be enough vertical room in the list to make way for a new .list-item
before it even begins animating into the list, and vice versa for the removal.
As for the styling, it’s very much like the Slide Down Opacity animation, only thing that the transition
for the .list-item
should be on the x-axis now instead of the y-axis.
.list-container {
cursor: pointer;
font-size: 3.5rem;
height: 0;
list-style: none;
position: relative;
text-align: center;
width: 300px;
}
.list-container.show:not(:first-child) {
margin-top: 10px;
}
.list-container .list-item {
background-color: #D3D3D3;
left: 0;
opacity: 0;
padding: 2rem 0;
position: absolute;
top: 0;
transform: translateX(300px);
transition: all 0.6s ease-out;
width: 100%;
}
.list-container .list-item.show {
opacity: 1;
transform: translateX(0);
}
As for the onclick
event handler of the addBtn
in our JavaScript, we’re using a nested setTimeout
method to delay the beginning of the listItem
animation by 350 milliseconds after its container
element has already started transitioning.
setTimeout(function(){
container.classList.add('show');
setTimeout(function(){
listItem.classList.add('show');
}, 350);
}, 10);
In the removeListItem
function, we remove the list item’s .show
class first so it can begin transitioning immediately. The parent container
element then loses its .show
class, but only 350 milliseconds after the initial listItem
transition has already started. Then, 600 milliseconds after the container
element starts to transition (or 950 milliseconds after the listItem
transition), we remove the container
element from the DOM because, by this point, both the listItem
and the container transitions should have come to an end.
function removeListItem(e){
let container = e.target;
while(!container.classList.contains('list-container')){
container = container.parentElement;
}
const listItem = container.querySelector('.list-item');
listItem.classList.remove('show');
setTimeout(function(){
container.classList.remove('show');
container.ontransitionend = function(){
container.remove();
}
}, 350);
}
Here is the end result:
That’s a wrap!
There you have it, three different methods for animating items that are added and removed from a stack. I hope that with these examples you are now confident to work in a situation where the DOM structure settles into a new position in reaction to an element that has either been added or removed from the DOM.
As you can see, there’s a lot of moving parts and things to consider. We started with that we expect from this type of movement in the real world and considered what happens to a group of elements when one of them is updated. It took a little balancing to transition between the showing and hiding states and which elements get them at specific times, but we got there. We even went so far as to make sure our list is both performant and accessible, things that we’d definitely need to handle on a real project.
Anyway, I wish you all the best in your future projects. And that’s all from me. Over and out.
The post Animation Techniques for Adding and Removing Items From a Stack appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.