Something interesting is happening in web development. People are realising that there are alternatives to the current JavaScript-based, heavy-client model of the web.
Technologies such as HTMX, Livewire and Hotwire keep most of the logic on the server, but allow responsive UIs by passing targeted snippets of HTML to the client. This allows developers to create UIs with a smooth, desktop feel, without having to maintain separate frontend and backend apps.
Here at Carwow we use Ruby on Rails, and are big fans of the Hotwire stack. In this article I'm going to explain an issue that we recently hit building a new page using Hotwire, and how we were able to get round it by using morphing.
Making a dynamic filters panel
We recently rebuilt our Product Listing Page in Hotwire (you can check it out here — we think it's a big improvement). The search filters panel is a big part of the page. To help you find your ideal car, you can apply filters such as price, body style, location etc to narrow down the results.
An important thing to note about our filters panel is that it is dynamic. It will only show filters that are valid options for the set of results that you are currently filtering for.
So, for example, if you select cars that cost more than £100k, the available "Makes" that you can filter by will change to only show the premium brands that offer cars at that price. To see more brands, you'll have to remove that price filter.

The filters panel seemed like a good choice for a turbo frame. It's a self-contained piece of UI, that needs to be replaced every time the user changed one of the filters.
Whenever a user changes a filter, the browser fetches a completely new set of HTML from the server, removes the existing filters panel element from the DOM and replaces it with the new one.
We have a small(-screen) problem
However, after working on the new filters panel for a while, we noticed a problem. On mobile, the filters panel covers the entire screen. If the user has a few filters open, then the panel will be larger than their viewport, and they will be relying on the scroll state of the filters panel element.
By making the panel a turbo frame, and replacing the whole element whenever any filter is updated, this scroll state was lost. As a result, the user was forced jarringly back to the top of the panel.
We experimented with manually trying to preserve the scroll state by storing it in Javascript and trying to restore it to the new turbo frame with Turbo lifecycle events. However, we were unable to make it work without some degree of noticeable flicker/movement.
Instead, we decided to investigate whether we could solve our problem with morphing.
What is morphing?
Building on technologies like Hotwire and HTMX, morphing is another recent technology that promises to further reduce the complexity of building frontends. Using fancy "DOM-diffing" algorithms, tools like idiomorph and morphdom compare the new HTML sent from the server with what is currently on the page. They then only change the elements of the page that have changed.
This is a huge leap forward in terms of reducing the complexity that the developer has to manage. We no longer needs to make a narrow request to the server, asking for a specific piece of the page to update. Instead, we can ask the server to render the whole page, then let the framework check the bits that have changed and only update those.
As an added bonus, it also allows us to preserve client-side state. With complex UIs, users interact with the page in ways that leave an impact on the page. When we replace page content wholesale, we remove the changes that the user has made (eg scroll state, focus etc). With morphing, all client-side state is preserved, so the user gets a "desktop app" feel.
This is why we were interested in using morphing for our mobile scroll state issue. By keeping the DOM intact as the turbo frame was rendered on the page, morphing could allow us to preserve the user's original scroll state.
Morphing in the Rails Stack
Turbo 8 was released in 2024, and introduced morphing to the library. As a reminder, Hotwire offers 3 options for updating a page:
- Turbo Drive — replace the whole page (minus the
<head>element) - Turbo Frames — partial page updates, triggered by user interactions within that part of the page
- Turbo Streams — partial page updates that can be triggered from anywhere, either from the server, or via JS elsewhere on the page.
After a bit of research, we discovered that, of these three, morphing has only been added to Turbo Drive and Turbo Streams.
This means that you can't morph a Turbo Frame.
Why can't you morph a Turbo Frame?
There are probably good reasons for why Turbo is set up this way, but I'm not entirely sure what they are. I raised an issue asking this question in the Turbo Github repo, but didn't get any good answers.
I do have my suspicions¹, but the important thing is that the feature was missing so this wasn't going to work.
It turns out that we don't need to wait for Turbo to add morphing to Turbo frames. While it won't work out of the box, we can add it ourselves, using Turbo's custom render function!
By hooking into Turbo's lifecycle methods, you can replace Turbo's default render function with whatever you want. This means, that can swap it out for a method that uses morphdom. As Turbo uses morphdom under the hood for its own morphing, this is a pretty great option.
Just add the code below somewhere into your application's javascript (eg in application.js)
import morphdom from "morphdom"
addEventListener("turbo:before-frame-render", (event) => {
event.detail.render = (currentElement, newElement) => {
morphdom(currentElement, newElement, { childrenOnly: true })
}
})turbo:before-frame-render is an event fired by Turbo whenever it is about to replace a turbo frame. Here we're stepping in at this point and replacing the default render function with our own custom one.
And there you have it - you too can now morph a Turbo frame!
The future of morphing
It will be interesting to see how Turbo evolves, and whether the maintainers do choose to bring morphing to Turbo frames in the future. Either way, it's already a fantastic tool for building maintainable, responsive UIs, and we're really enjoying learning how to use it.
- I think the reason we haven't seen morphing in Turbo Frames yet is probably because:
- Turbo drive involves a full page-load, but only requires changing a single node in the page — the document body. Nice and simple.
- Turbo streams do not involve a full page-load. The history stays the same, some of the DOM is updated. Also nice and simple.
- Turbo-frames are a curious combination of the other two. They involve a partial page-refresh, but also have characteristics of a full page-load (eg updating history). The combination of these two things could lead to a bunch of edge cases when Turbo looks to run post-navigation checks (eg searching for duplicate IDs, re-evaluating scripts etc).
But this is just a guess.