Animating transitions between two different DOM states is hard, even for the simplest things. The View Transitions API is an experimental tool that provides a much simpler way to do this, all while updating the DOM contents in a single step.

The API takes two snapshots of the content when a page is updated, one with the old state and one with the new state. It then builds a tree of pseudo-elements like the one below, which you can use to animate the in-between state.

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

To enable this, all you have to do is to wrap the function modifying the DOM in document.startViewTransition(). The coolest thing is that it works for all kinds of updates, from changing classes and adding/removing elements to tab changes and page routing.

function changeTab(data) {
  if (!document.startViewTransition) {
    updateDOM(data)
    return
  }

  document.startViewTransition(() => updateDOM(data))
}

For now, it only works in applications that use JavaScript to update content, but the plan is to support multi-page applications as well. You can read more about the API here, here, here, and here.

Since we’re talking about reducing the complexity of doing things, what if we could use this technique with Phoenix LiveView?

Disclaimer: The following content is an experiment. LiveView doesn’t support view transitions yet. I played with it on a beautiful morning after Christmas for learning purposes. Proceed with caution.

Hacking LiveView

The first thing is to find out where the lib updates the content after interacting with a LiveView. After a few debugging sessions using the Chrome inspector, I discovered the View.applyDiff() method in view.js. I’ve changed the code as follows:

/* phoenix_live_view/assets/js/phoenix_live_view/view.js */

// ...
applyDiff(type, rawDiff, callback){
  this.log(type, () => ["", clone(rawDiff)])
  let {diff, reply, events, title} = Rendered.extract(rawDiff)

  if (document.startViewTransition) {
    document.startViewTransition(() => callback({diff, reply, events}))
  } else {
    callback({diff, reply, events})
  }
  if(title){ window.requestAnimationFrame(() => DOM.putTitle(title)) }
}

I’m not sure if this is the best place to add the code, but the important thing is that it is sufficient for our purposes. Unfortunately, simply updating the code in this way won’t work. The core team may have a build process that bundles everything together, and I think that the process is not available in the repository, which is perfectly fine.

So, if you really want to try it, you have two options:

  1. Come up with a custom build process to bundle everything together in a fork, and replace the phoenix_live_view dependency in mix.exs with your own;
  2. Run the Phoenix server, then open the priv/static/assets/app.js, find the applyDiff() method and change it to match the chunk of code from above. It’s a silly approach, I know. Every time the assets are recompiled, the changes are discarded.

I chose the 2nd one for the sake of science and experimentation.

LiveView Transitions in Action

I’ve used the Thermostat Example from the official docs to validate the experiment. Browsers add a nice cross-fade effect by default if you wrap your DOM update in a document.startViewTransition call. Take a look at the videos below to check the differences.

The sky is the limit from here. You can play with the pseudo-elements created by the API to build complex animations to your updates. You can even use different animations for each element involved in the change - that’s the reason the view-transition-name property exists.

Next Steps

The View Transitions API has a decent support so far, as it already works in both Chrome and Edge. The specification is now at Candidate Recommendation status, so it may be available in the near future. Let’s hope it happens quickly.

See you in the next post!