<transition-group>
...
</transition-group>
        

github logo

When UI elements are created, destroyed or change position, animating the changes can help users understand what is happening. The transition: css directive is handy for this. But applying it to the right DOM elements at the right time can be challenging. <transition-group> is here to help!


<transition-group>
  <div class="transition-this">
    ...
  </div>
</transition-group>






            

<transition-group> is implemented as a web component so that you can easily use it as a regular html-tag, with any framework - or none. Install webcomponent-transition-group via npm and import where needed:


import 'webcomponent-transition-group'
            

Or simply import it directly into your html from CDN:


<script
  type="module"
  src="https://unpkg.com/webcomponent-transition-group"
></script>
            

When you don't need it

If you have an element already rendered on the page, making it appear and disappear is a simple matter adding the transition directive to it, and toggling the hidden state of it on or off with another class.

Click the blue tab below for an example:

Here is some secret content That appeared Here is some secret content That appeared Here is some secret content That appeared Here is some secret content That appeared

It gets trickier when you don't know the content until it's time to make it appear. Then you need to paint it once in the hidden state, and on the next paint apply the transition directive while simultaneously removing the hiding directives.


<button
  onclick="
    document
      .querySelector('#curtain')
      .classList
      .toggle('hidden')
  ">
    ...
</button>
            

  #curtain {
    ...
    transition: 0.4s;
  }
  #curtain.hidden {
    transform: translateX(-100%);
  }
            

Entry transitions

That is the first thing <transition-group> can help with. Input some text in the box to the left and click '+'. Watch how it enters the list smoothly.

The effect is achieved by wrapping the list of items to render in in <transition-group> and setting the entry attribute to a class-name.

Every dom element added to the group in this case, will get the item--entry-pre class. Use that class to define how the element is hidden before entry. On the next paint, the "-pre" class will be replaced by item--entry where you define how the element enters.


<ul>
  <transition-group entry="item--entry">
    <li class="item">...</li>
    <li class="item">...</li>
    ...    
  </transition-group>
</ul>



            

  .item {
    /* regular layout directives here */
  }
  .item.item--entry-pre {
    transform: translateY(-100%);
    opacity: 0;
  }
  .item.item--entry {
    transition: 0.4s;
  }
            

Slide Transitions

You'll notice in the example above that even though new items enter smoothly, the already existing list items 'skip' to their new positions in a janky fashion. We can fix this by adding a slide="item--slide" attribute.

This will cause all existing elements to be painted in their original positions first, and then transitioned to their new positions according to the transition defined in the class item--slide

Try adding and removing elements in the example below to observe the effect.


<ul>
  <transition-group
    entry="item--entry"
    slide="item--slide"
  >
    <li class="item">...</li>
    <li class="item">...</li>
    ...    
  </transition-group>
</ul>
          

.item.item--slide {
  transition: 0.4s;
}
            

Exit Transitions

The one thing left is to enable exit transitions. It probably doesn't come as a huge suriprise that this is enabled by the exit attribute

Notice in the example how the additions above make the element pop away in a pleasing fashion.


<ul>
  <transition-group
    entry="item--entry"
    slide="item--slide"
    exit="item--exit"
  >
    <li class="item">...</li>
    <li class="item">...</li>
    ...    
  </transition-group>
</ul>
              

.item.item--exit {
  transform: scale(2,2);
  opacity: 0;
  transition: 0.4s;
}
            

Alternating transition classes

In this example, the slides are changed by exit-transitioning the old one and enter-transitioning the new one. The enter and exit attributes are set to different values depending on wether we are going left or right in the sequence.


let tg = document.querySelector('transition-group.slideshow')
let bl = document.querySelector('button.slideshow__left')
let br = document.querySelector('button.slideshow__right')
bl.addEventListener('click', () => {
  tg.setAttribute('entry', 'slideshow--entry-left')
  tg.setAttribuge('exit', 'slideshow--exit-left')
  //... swap current slide for left one
})
br.addEventListener('click', () => {
  tg.setAttribute('entry', 'slideshow--entry-right')
  tg.setAttribuge('exit', 'slideshow--exit-right')
  //... swap current slide for right one
})



            

.slideshow--entry-left-pre,
.slideshow--exit-right {
  transform: translateX(-100%);
  opacity: 0;
}
.slideshow--entry-right-pre,
.slideshow--exit-left {
  transform: translateX(100%);
  opacity: 0;
}
.slideshow--entry-right,
.slideshow--entry-left,
.slideshow--exit-left,
.slideshow--exit-right {
  transition: 0.5s ease-in-out;
}
            

Slide transitions are neat

Ok so this example doesn't really add anything. It's just cool I think.

At first brush, one might think that the animations for the squares are individually and intentionally calculated by the app code. But actually the squares are all layed out usting display: flex and flex-wrap: wrap, in one long list.

After the player selects a move, the new position of all the squares (including the blank), are calculated, the view is updated, and the slide-transition takes care of the sliding effect.

Notes & Caveats

When you use one of the entry, slide, or exit on a transition group, make sure there is a transition: directive in those class-names. If no transition is triggered even though the attributes are set, the behavior is undefined (and you'll see strange effects).

The slide & exit effects depend on keeping track of the positions of elements in a transition group. The position is checked each time the children of the transition group is changed/altered, as well as if resize or scroll events are triggered on the window object. If the position of the elements are altered in some other way, make sure to trigger one of those events or the transitions may behave strangely (moving from/to strange positions)

Exit transitions work by keeping the elements that were removed in the shadow-DOM. This allows us to keep them visible as they dissapear, without interfering with virtual-DOM-based frameworks who often expect full control of the DOM. However this leads to problem with styling, as styles that apply to the "light DOM" (not-shadow-dom) do not apply to shadow DOM elements.

In order to handle this, we actually clone every style element on the page (<link href="stylesheet"> and <style>) inside the shadow-DOM of each transition-group. If this seems inefficient to you - I don't disagree. It's just the only way, as far as I know, to make the same styling an element had in the light DOM apply also in the shadow DOM.

However, since the element to be removed is moved out of it's ordinary structure, you must make sure that the style-rules for transition-group-elements are not dependent on them having certain ancestor-nodes. Because those rules won't apply during exit.