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.