Animating Turbo Streams with Animate.css

We'll use the Animate.css library to simplify our work.

Animate.css

Now that we have some experience with animations, let’s revisit the transitions we built in Turbolinks with Turbo Streams. As a reminder, our current implementation has new elements appearing in our favorites list and old ones being removed. This works, but it’d be more interesting to animate those transitions.

We can do this, but managing the outgoing transitions takes a little bit more work.

We’re going to call in a helper for the animation rather than continue to code the transitions by hand. The Animate.css library adds quite a few useful CSS animations that are a couple of CSS classes away.

The install process consists of two steps. First, add the package using yarn:

yarn add animate.css

Now we can animate our transitions by simply adding the CSS class animate__animated to any element and then following that with one of the several specific animation classes that Animate.css provides, like animate__fadeInUp.

Going back to our Turbo Streams example, when we add something to the favorites list, we’d like it to animate in. All we need to do in that case is add the Animate.css classes to the response HTML that the Turbo Stream sends back when we make a concert a favorite.

This snippet has changes for both the animate in and animate out in the _favorite partial:

<article class="my-6 animate__animated animate__slideInRight"
         id="<%= dom_id(favorite) %>"
         data-animate-out="animate__slideOutRight">

To animate in, we can add the CSS classes animate__animated animate__slideInRight to the article tag that surrounds the favorite listing. A side effect of this code as written is that on an ordinary page load, all the existing favorites will slide in from the right. If that is bothersome, then you need to add another local variable so you can distinguish between “on page load” and “on turbo stream request” and add the animate classes conditionally.

When we make a new favorite concert, the article will appear to slide in from the right.

Problem with Animating

The problem with animating the removal of the concert is that we need to make sure the animation happens before Turbo Stream removes the element. Otherwise, our animation will not be visible.

To make that happen, we need to capture the Turbo Stream event before it’s rendered. Then we can add the CSS classes and trigger the DOM removal ourselves after the animation completes.

Turbo provides an event hook for just this purpose called turbo:before-stream-render. The event is triggered after a Turbo Stream response is returned to the client but before Turbo Stream does anything with that response.

Here’s a code that does what we want. To start, if we look at the earlier code, a "data-animate-out": "animate__slideOutRight" was added to the turbo-frame tag. This makes the animation choice data-driven, and it allows us to distinguish between Turbo Stream removals with an animation, which will have this data element defined, and those without an animation, which will not.

Here, we’re adding an event listener for turbo:before-stream-render:

document.addEventListener("turbo:before-stream-render", (event) => {
  if (event.target.action === "remove") {
    const targetFrame = document.getElementById(event.target.target)
    if (targetFrame.dataset.animateOut) {
      event.preventDefault()
      const elementBeingAnimated = targetFrame
      elementBeingAnimated.classList.add(targetFrame.dataset.animateOut)
      elementBeingAnimated.addEventListener("animationend", () => {
        targetFrame.remove()
      })
    }
  }
})

The event is passed to the callback function by Turbo: the event target is the code being returned. We get one of these callbacks for each Turbo Stream, so if your HTML response combines multiple requests, as ours does, you’ll call this event multiple times.

Stream event.target.action

The first thing we do is check the action of the stream event.target.action, which is the action= attribute of the incoming stream. We only care about removals for this part, so we only proceed if the action equals remove.

Next, we pull in the target frame ID, which is the target attribute of the event.target or event.target.target. Remember, the Turbo Stream looks like <turbo-stream action="remove" target="fav_concert_40">. We use document.getElementById to get the actual element on the page with that DOM ID, which is the Turbo Frame element we are planning to remove.

data-animate-out Attribute

If the Turbo Frame element has a data-animate-out attribute, we know we want to animate it. Inside that if block, we first use preventDefault(). Doing so keeps the event from being propagated, which in our case prevents Turbo from removing the element before we’re done with it.

We then grab the element referenced by the target frame. In our case that’s the article element that already has animate__animated added to it. This code assumes that the element being removed will be both a top-level element containing the entire contents of the frame and will already have animate__animated.

We take the value of targetFrame.dataset.animateOut, which we set to animate__slideOutRight, and we add it to the child element. We also add a listener to that element for the event animationend, which is fired by the DOM when an animation ends. Inside that listener, we remove the targetFrame from the DOM by ourselves, which is what Turbo would have done with it anyway.

This should work. When we hit the button to remove a favorite, an HTTP request is sent and the response is a Turbo Stream that fires the turbo:before-stream-render event. Our listener captures that event, verifies the data, adds the correct CSS animation class to the top-level element of the stream data, listens for the end of the animation, and finally removes the element.

This code is reusable. Anywhere we think a Turbo Stream might remove a frame, we can augment that frame with a data-animate-out attribute, and this listener will apply that animation when something is removed.

Here’s the application we have so far:

Get hands-on with 1200+ tech skills courses.