Interactive without a single-page app
Turbo gets you most of what a client-side framework does, with the state still on the server where it is easier to reason about.
The default assumption is that interactivity requires a client-side framework, and with it a second application, a second state model and a serialisation layer between them.
Turbo covers a large fraction of that without any of it.
Three mechanisms, in order of how often you need them
Turbo Drive intercepts navigation and replaces the body rather than reloading. You get the feel of a client-side app for free, on every link, with no code. Most of the perceived benefit of an SPA is this.
Turbo Frames scope an update to a region. A link inside a frame replaces only that frame. Inline editing, tabs, pagination within a section — all become ordinary server responses that happen to target a smaller area.
Turbo Streams send targeted operations — append, replace, remove — in response to an action or over a WebSocket. This is where live updates come from without writing a client-side store.
What you keep
One application. One place where authorization lives. One template layer. The state is in the database, and the server sends HTML that reflects it, which means there is no possibility of client and server disagreeing about what is true.
That last point is the real saving. A substantial share of front-end complexity is keeping a client-side copy of server state correct.
Most SPA complexity is state synchronisation. Sending HTML removes the second copy.
Where it genuinely does not fit
Interfaces with rich local state that must survive without a round trip — a drawing canvas, a spreadsheet, a real-time editor. Anything that must work offline. Anything where a native mobile client shares the API.
For those, a client-side framework earns its complexity. The mistake is reaching for it by default when the requirement is a form, a filter and a modal.
The parts that trip people
Turbo caches a preview of the page for instant back navigation. Anything that mutates the DOM after load will appear briefly in that cached state, which produces the characteristic flash of a stale widget. There are attributes to exclude elements from caching, and knowing they exist saves an afternoon.
Element ids matter, because frames and streams target them. Duplicated or generated ids cause updates to land in the wrong place, and the failure is silent.
And JavaScript that runs on page load will not re-run on a Turbo navigation, because there was no page load. That is what Stimulus controllers are for — they connect and disconnect with the elements they are attached to, which is the model that survives partial page updates.