13 · page refreshes

The most common navigation is to where you already are.

Submit a form, get redirected back to the same page. Turbo calls that a page refresh. With Drive alone, it's a full body swap: scroll to the top, panel snaps shut, anything open closes.

Try it. Scroll the list down a bit, open the Related panel on the right, then post a message.

→ scroll down, open Related, post
13 · morphing

Two meta tags.

<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">

Now when a refresh lands, Turbo walks the old DOM and the new HTML side by side and only touches the nodes that differ. It uses a library called idiomorph to do the walking. Nothing else is rebuilt, so scroll stays, and text in the field you're typing in stays.

Flip both on. Scroll down, open the panel, post again. Watch which parts flash.

The server still sends the whole page. Look at the response: the same HTML as before, but only the differences got used.

→ switch both on, scroll, open Related, post
13 · the panel still closed

Morphing can only keep what the HTML knows.

Scroll survived. The Related panel closed anyway. Why?

An open <details> is an open attribute on the element. The server never knew the panel was open, so its HTML has no open. The morph faithfully removed it. Same story for a dropdown you toggled with JavaScript, a video mid-play, a third-party widget that built its own DOM.

The fix is the attribute you already met: data-turbo-permanent excludes an element from the morph entirely. Flip it, open the panel, post.

Frames get their own knob: <turbo-frame refresh="morph"> reloads a frame by morphing instead of replacing it, so content you loaded later (page 2 of a list, say) doesn't vanish on refresh.

13 · the lazy path to live updates

Once refreshes are gentle, you can trigger them from anywhere.

There is a stream action called refresh. It means "whatever page you're on, refresh it". Send it over a WebSocket, a connection the page keeps open so the server can push without being asked, and morphing turns it into a one-line real-time feature in Rails:

# app/models/message.rb
class Message < ApplicationRecord
  belongs_to :board
  broadcasts_refreshes_to :board
end

<%# app/views/messages/index.html.erb %>
<%= turbo_stream_from @board %>

Every tab subscribed to that board gets a <turbo-stream action="refresh"> over the WebSocket whenever a message changes, and each tab morphs itself to match. No partials to name, no targets to pick. Coarse, but often exactly enough.

The precise version, where the server says which elements change, is two rungs up. First, a smaller box.