In-N-Out Animation using sibling-index()
In-N-Out Animation using sibling-index() êŽë š
Chris shared a nice series of articles focused on animating elements in and out of view. The first two articles were about popups & dialogs that required no JavaScript. CSS gives us native ways to open/close those elements while having cool animations. The third article was about view transitions, and while itâs a powerful CSS feature, some JavaScript code is still required to make it work.
Here is the original demo where you can add/remove items and everything adjusts smoothly:
All the effects are managed with CSS, and the JavaScript is just the basic code to trigger the view transitions. Compared to some heavy JS libraries, itâs pretty minimal, and thatâs cool.
In this article, I will explore another CSS-only idea that doesnât require JavaScript (except for the one to add/remove DOM elements).
Note
At the time of writing, only Chrome and Edge fully support the features we will be using.
Here is my version of the same demo:
Itâs not exactly the same as the original demo, but the CSS code required to achieve that effect is pretty small and relies mainly on one feature: sibling-index().
I suppose you are curious to know how it works, right? Letâs get started!
The Technique
The sibling-index() function in CSS returns the index of an element among its siblings. We generally consider the index to be static/fixed, which is the case in most situations, but what if we add/remove elements? The index gets updated to match the new HTML structure.
Take the following example:
<div class="container">
<div>red</div> <!-- index = 1 -->
<div>green</div> <!-- index = 2 -->
<div>blue</div> <!-- index = 3 -->
<div>yellow</div> <!-- index = 4 -->
</div>
If we remove the âgreenâ element, the structure becomes:
<div class="container">
<div>red</div> <!-- index = 1 -->
<div>blue</div> <!-- index = 2 -->
<div>yellow</div> <!-- index = 3 -->
</div>
The index of âblueâ and âyellowâ have changed. If I use the index within a property and I apply a transition to that property, I will get an animation.
Here is a basic demo. Click remove and see what happens:
The CSS code is as simple as:
.container {
display: grid;
}
.container div {
grid-area: 1/1;
width: 150px;
translate: calc((sibling-index() - 1)*160px);
transition: .4s;
}
All the elements are placed on top of each other (using grid-area: 1/1), then translated using their indexes.
If an element is removed/added, the indexes are updated, and the translate values are recalculated, creating a nice animation thanks to the transition. With that simple trick, we can create in-and-out animations!
The Demo
Letâs get back to our demo and understand whatâs going on. Here is the main CSS code:
ul {
display: grid;
}
ul > li {
--g: .5rem; /* gap between items */
grid-area: 1/1;
translate: 0 calc((sibling-index() - 1)*(100% + var(--g)));
transition: .5s;
@starting-style {
opacity: 0;
transform: translatex(-80%);
}
}
I simply added a @starting-style declaration. Thatâs what controls the entry effect when a new element is added to the DOM. I am using opacity combined with a left translation to match the demo created by Chris, but it can be anything.
Here is a different entry effect using scale and opacity:
Thatâs all. With less than 10 lines of CSS, I managed to get a cool effect.
The Drawbacks
Now, letâs talk about the drawbacks of this method. I am using translate to place the elements, which means we are working with out-of-flow content. In our case, the width of the list is good, but the height is not, and the items are overflowing their container. In some situations, itâs fine, but in others, we may need to find some workarounds to avoid this overflow.
About the height, all the items need to have the same height, otherwise we cannot apply this technique. Even if the items donât have the same height, I am forcing that by using grid-area: 1/1. By placing all the items in the same grid area, I will get an area with a height equal to the tallest item and all items will, by default, get stretched inside that area.
Here is the previous demo where I made the second item bigger:
All the items will follow that height (unless you change the alignment). Itâs still a drawback, but at least the layout is not broken, and everything works fine.
In our demo, it was about height, but in other demos, it can be width, so letâs say there is a fixed size to respect (either the height, the width, or both).
The last drawback is that we donât really have an out animation. Removing an element will adjust all the others perfectly, but the removed element has no animation. Itâs not really a drawback because we donât have a native CSS mechanism to do this like @starting-style (maybe an @ending-style in the future?). For this reason, Chris explored the view transition method that allows us to achieve a real out animation.
Note
Thatâs a lot of drawbacks!
Yes, but given the code we used (barely 6 lines of CSS), itâs still a great CSS trick that can create many cool demos. Speaking about demos, letâs have a look at some of them!
More Examples
Here is a list of avatars within a container. For this one, we donât have any sizing drawbacks. The container height is good (it follows the image height), and we have a full-width configuration.
Since the width of the container doesnât depend on its content, I made it a container and used cqw to create a responsive behavior where the image may overlap to always fit within the container, regardless of the total number.

For that, I defined a slightly complex translate value that depends on the container size (100cqw) and the number of items (sibling-count())
translate: calc((sibling-index() - 1)*min(100% + var(--g),100cqw/sibling-count() - (100% - 100cqw/sibling-count())/(sibling-count() - 1))) 0;
Here is another idea with a circular list of avatars. This time, I am not using translate but offset combined with circle(). For this demo, the circle expands to prevent the images from overlapping.
And if you donât want the circle to get bigger, you can use a fixed radius value. Doing this gives us a similar behavior to the first example: an overlap between the images.
Letâs try with a grid of items. By knowing the container width (using cqw), and the item size, we can calculate the coordinate of each item within the grid and place them using translate:
.container {
--g: 10px; /*gap */
--s: 130px; /* image size */
display: grid;
container-type: inline-size;
}
.container img {
grid-area: 1/1;
width: var(--s);
/* the number of items that can fit inside one row */
--n: round(down,(100cqw + var(--g))/(var(--s) + var(--g)));
/* the item coordiantes */
--i: round(down,(sibling-index() - 1)/var(--n));
--j: mod(sibling-index() - 1,var(--n));
translate:
calc(var(--j)*(100% + var(--g)))
calc(var(--i)*(100% + var(--g)));
}
In addition to the nice effect we get when adding/removing items, we also get a nice transition when you resize the screen. Try it!
I was also able to overcome the height drawback by using a hidden <div> inside the grid as a last child. By placing it in the correct row, I ensure the container grows to accommodate all the elements. I wonât get into the fine details of this hack, but it illustrates that, depending on the use case, we can find workarounds to address some of the drawbacks we listed previously.
Conclusion
Respect to sibling-index()! Now, you know the superpower of this feature. Getting the index of an element? Nah, making cool demos featuring in-and-out animations!
Donât forget to check the article series by Chris, especially the third one. It would be a good exercise to implement my demos using the view transition technique and compare both methods.