<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<div class="posts-index">
<div class="sticky top-0 bg-white border-b" id="search-bar">
<%= form_with url: posts_path, method: :get, data: { turbo_frame: "_top", turbo_action: "morph" } do |f| %>
<%= f.search_field :q,
value: params[:q],
placeholder: "Search posts...",
class: "form-input",
data: { action: "input->debounce#submit" } %>
<% end %>
</div>
<!-- Video player that should persist across navigations -->
<div id="persistent-player" class="mb-6">
<video id="promo-video" controls>
<source src="<%= asset_path('promo.mp4') %>" type="video/mp4">
</video>
</div>
<div id="posts-list">
<%= render @posts %>
</div>
<div id="pagination">
<%= paginate @posts %>
</div>
</div>
<article id="<%= dom_id(post) %>" class="post-card">
<h2>
<%= link_to post.title, post_path(post), data: { turbo_action: "morph" } %>
</h2>
<p class="text-gray-600"><%= post.excerpt %></p>
<div class="post-meta">
<span><%= post.author.name %></span>
<span><%= time_ago_in_words post.created_at %> ago</span>
</div>
</article>
Traditional Turbo Drive replaces the entire <body>, which can cause flickering and lose scroll position. Turbo Morphing (using idiomorph algorithm) intelligently diffs the DOM and updates only changed elements, preserving focus, scroll, and transient state like video playback. I enable morphing with data-turbo-action='morph' on links or forms. This is particularly valuable for pages with persistent elements like media players, chat widgets, or partially filled forms. Morphing works best when element IDs are stable across renders—I use dom_id(record) helpers to ensure consistency. The tradeoff is slightly more CPU usage for diffing, but the improved UX is worth it for interactive pages.