How to Ruby logo

How to effortlessly animate html elements between Turbo page changes

01 Aug 2022

Html declarative turbo animations

Combine Turbo and el-transition to easily add nice animation transitions without imperative JavaScript manipulations.

Step 1

Add el-transition

yarn add el-transition

Step 2

Add a JS file to use el-transition to apply transitions to all incoming and outgoing elemenets with data-transition-enter|leave data tags.

// app/javascript/src/turbo-animate.js
import { enter, leave } from 'el-transition'

// Animate outgoing and incoming elements between pages
//
// <!-- declare enter and leave anmiations using data attributes -->
// <div id="dropdown-menu" class="menu hidden"
//     data-transition-enter="transition ease-out duration-100"
//     data-transition-enter-start="transform opacity-0 scale-95"
//     data-transition-enter-end="transform opacity-100 scale-100"
//     data-transition-leave="transition ease-in duration-75"
//     data-transition-leave-start="transform opacity-100 scale-100"
//     data-transition-leave-end="transform opacity-0 scale-95"
// >
//   <!-- html -->
// </div>

let leavePromises = []

document.addEventListener('turbo:before-fetch-request', () => {
  disappearAll()
})

window.addEventListener('popstate', () => {
  disappearAll()
})

document.addEventListener('turbo:before-render', async (event) => {
  if (leavePromises.length == 0) return

  event.preventDefault()

  await Promise.all(leavePromises)

  // Cleanup and cache page again
  document.querySelectorAll('[data-transition-leave]').forEach((element) => {
    element.classList.remove('hidden')
  })

  Turbo.session.navigator.view.cacheSnapshot()

  event.detail.resume()
})

document.addEventListener('turbo:render', () => {
  appearAll()
})

const disappearAll = () => {
  leavePromises = Array.from(
    document.querySelectorAll('[data-transition-leave]')
  ).map((element) => leave(element))
}

const appearAll = () => {
  document.querySelectorAll('[data-transition-enter]').forEach((element) => {
    enter(element)
  })
}

Step 3

Add data-transition-* markup

<!-- page 1 -->

<%= form_with url: posts_path do |f| %>
  <div class="flex flex-col items-center justify-center"
    data-transition-leave="transition ease-in duration-1000"
    data-transition-leave-start="transform opacity-100 scale-100"
    data-transition-leave-end="transform opacity-0 scale-50"
    data-recording-target="recordedState"
    >
    <button type="submit" class="rounded-full w-40 h-40 flex items-center justify-center font-bold text-xl bg-purple-500 text-white shadow">
      Submit
    </button>
  </div>
<% end %>
<!-- page 2 -->

<div data-transition-enter="transition ease-out duration-1000"
  data-transition-enter-start="transform opacity-0 scale-50"
  data-transition-enter-end="transform opacity-100 scale-100"
  >
  <%= image_tag "illustrations/happy-smiley-with-flowers.png" %>
</div>

Use cases

I find the simplicity of this appoach to have great merits. If combined with Turbo Frames - any small section of the app, or an icon button such as “Favourite” or “Bookmark”, can be nicely animated to pop out and back in as it changes state.

It won’t be difficult to employ the same appraoch to handle animations instead of transitions for more complex effects. See tailwindcss-animate for declarative animation classes.

Hope this helps!

Enjoy! 😊