60 · the ladder, filled in

You climbed this.

The same question, answered six times. Every row is something you did with your own hands. Click a name to go back to it.

toolthrows awaykeepswho decidesblast radius
page loadthe whole tab processnothingthe browser
Turbo Drivethe <body>the JS process, loaded CSS/JS, data-turbo-permanent elementsTurbo, for every same-site link
morphingonly nodes that changedscroll, typed text, everything the HTML agrees onthe diff between old and new HTML
Turbo Framesthe inside of one boxthe rest of the page, and the URLthe id you put on both pages
Turbo Streamsnamed elements, wherever they areeverything else; no navigation at allthe server, per element
Stimulusnothing—; it attaches behaviour to whatever HTML landsthe HTML, via data-controller
Nativenothing on the web sideevery page; wraps them in a native stackthe path configuration

Dashed bar: no web-side change, a native shell around it. Not on the table because they're not tools: the <head> merge, the cache, and the 303 rule. They're the ground the tools stand on.

60 · the rule

Four questions pick the tool.

askif yes
Is it a navigation to another page?Drive. It's already on. Done.
Is it a navigation back to the page you're on?morph refresh, so nothing jolts
Does one region need to navigate on its own?a Frame around that region
Must several places change, or other people's screens, or nothing navigate at all?Streams: from the response, or broadcast over the socket
Is no server involved?Stimulus
Is it a phone?Native, wrapping all of the above

Ask them in order; stop at the first yes. Now try it on twelve real situations.

60 · the decision desk

Twelve situations from a real Rails app.

Place your bet, then read why. No score matters; the reasons do.

your bets

60 · sandbox

Every switch, at once.

Ask your own question. The model is honest about what it does, and honest about what it isn't: the morph is id-keyed and simpler than idiomorph, there's no real network so no latency games, and there's no WebSocket here (that lives in 32). Flip anything; the next request picks it up.

Questions to try:
60 · cheat sheet

One card. Everything you touched.

Drive

data-turbo="false"
hand a link, form, or container back to the browser
data-turbo-action="advance | replace"
push or replace history; on a frame, promotes its navigations to real visits
data-turbo-permanent + id
carry the node across renders; also excluded from morphs
data-turbo-temporary
remove before caching (flash messages)
data-turbo-track="reload"
on fingerprinted assets: full reload when they change
data-turbo-method / data-turbo-confirm
non-GET links, with a confirm
data-turbo-submits-with
label while submitting
data-turbo-prefetch="false"
opt a link out of hover prefetch
data-turbo-preload
warm the cache for a link before it's clicked

Meta tags

turbo-refresh-method=morph
refreshes morph instead of replacing body
turbo-refresh-scroll=preserve
keep scroll on refresh
turbo-visit-control=reload
this page always does a full load (login pages; frame breakout)
turbo-cache-control=no-cache | no-preview
opt a page out of the cache or just out of previews
turbo-prefetch=false
disable hover prefetch site-wide
turbo-root
scope Drive to a path prefix
view-transition=same-origin
animate with the View Transition API

Frames

turbo_frame_tag model
<turbo-frame id="dom_id">; same id on both sides
src="/path"
eager-load the frame after the page
loading="lazy"
load when scrolled into view
target="_top"
links inside drive the page, not the frame
data-turbo-frame="id | _top | _self"
on a link or form: which frame to drive
refresh="morph"
morph the frame on page refresh instead of reloading it
turbo_page_requires_reload
Rails helper for visit-control reload (login page)
turbo:frame-missing
event when the response had no matching frame

Streams

append prepend replace update remove before after refresh
the eight actions; method="morph" on replace/update
target="id" / targets="css"
one element by id, or many by selector
format.turbo_stream
respond_to branch; renders create.turbo_stream.erb
turbo_stream.append "messages", @message
reuse the _message partial
data-turbo-stream
accept streams on a GET link or form
turbo_stream_from @board
subscribe the page over Action Cable
broadcasts_to :board / broadcasts_refreshes
model-side: precise streams, or a morph refresh

Forms

303 See Other
after a successful non-GET, redirect
422 Unprocessable Content
to re-render a form with errors
200 from POST
ignored by Turbo, on purpose
turbo:submit-start / submit-end
events on the form

Stimulus

data-controller="name"
name_controller.js attaches here
data-name-target="x"
static targets = ["x"] → this.xTarget, xTargets, hasXTarget
data-action="event->name#method"
event optional for button/form/input; .esc, @window, :prevent, :stop
data-name-x-value
static values = { x: Number } → this.xValue, xValueChanged()
data-name-x-class
static classes = ["x"] → this.xClass
data-name-x-param
event.params.x, read from the element carrying the action
data-name-other-outlet=".sel"
static outlets = ["other"] → this.otherOutlet(s)
initialize connect disconnect
lifecycle; [x]TargetConnected too
this.dispatch("event")
fires name:event for other controllers

Native

path configuration
JSON rules by URL regex: context modal, presentation replace/pop/clear_all/refresh
data-turbo-action="replace"
replace the top screen
BridgeComponent + static component
a Stimulus controller that talks to Swift/Kotlin via this.send()
data-bridge-title, data-bridge-disabled
element hints for the native side
data-controller-optout-ios/android
disable a component per platform