Ruby on Rails Module 5 – Hotwire: Turbo, Stimulus and real-time
Turbo Drive and Turbo Frames
Follow Turbo in a Rails 8.1 app: the Drive body swap, the statuses forms must return, inline editing in a frame, lazy frames and morphing refreshes.
What you will learn
- Trace one Turbo Drive visit from a click through the head merge and body swap to the browser history
- Explain the statuses a form must return to Turbo, and when to opt out, track assets or let links prefetch
- Use a Turbo Frame to scope navigation, including inline editing, lazy loading and breaking out to the whole page
- Decide when a page refresh should morph, and add the focus and live region a frame swap needs
Before you start
What you’ll learn · Before you start
What you will learn
- Trace one Turbo Drive visit from a click through the head merge and body swap to the browser history
- Explain the statuses a form must return to Turbo, and when to opt out, track assets or let links prefetch
- Use a Turbo Frame to scope navigation, including inline editing, lazy loading and breaking out to the whole page
- Decide when a page refresh should morph, and add the focus and live region a frame swap needs
Before you start
On this page
A visitor edits one batch name in Kaksha, the coaching-centre app this track builds. Without Turbo, saving that one field reloads the whole page: 28 middleware, the full layout and every stylesheet and script, just to change three words in a table. Turbo Drive and Turbo Frames remove that cost while you keep writing plain, server-rendered Rails.
This lesson follows recorded requests through a Rails 8.1 app running turbo-rails 2.0.23 (Turbo 8.0.23). You will trace one Drive visit, see the statuses your forms must return, scope an edit to a single row with a frame, defer work with a lazy frame, keep a refresh smooth with morphing, and add the focus and announcements a frame swap leaves out.
Turbo Drive turns a click into a visit
A new Rails app loads Turbo in app/javascript/application.js, so Drive is on from the first page. Turbo Drive: The part of Turbo that intercepts link clicks and form submissions, fetches the page in the background, and updates it by swapping the body and merging the head instead of a full reload. watches for link clicks and form submissions, performs them in the background, and
updates the page without a full reload. It models each one as a visit to a URL.
Follow one visit to /batches/7, from the click to the rendered page:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A click starts a visit. A visitor clicks a link that stays on the same origin. Turbo Drive catches the click instead of letting the browser leave the page.
- Fetch, page still on screen. Turbo fetches the target URL in the background. The current page stays visible, and a thin progress bar appears if the request takes longer than half a second.
- Merge the head. When the response arrives, Turbo merges the two documents' head elements. A stylesheet or script whose URL has not changed is left alone, so the browser never re-parses it.
- Swap the body. Turbo replaces the current body with the response body and updates the html lang attribute if it differs. The page is now the new one, without a reload.
- Record the visit. Turbo pushes a new browser history entry for an advance visit and keeps a copy of the page in its cache, so the next visit here can show a preview at once.
The render step is the heart of it. Turbo replaces the current document’s <body> with the response’s <body>, merges
the two <head> elements, and updates the <html> lang attribute if it changed. Merging the head matters: a
stylesheet or script whose URL has not changed is left untouched, so the browser never parses it again, which is why a
Turbo visit feels so much lighter than a reload.
A visit carries an action. The default, advance, pushes a new browser history entry; replace swaps the current
one instead, chosen with data-turbo-action="replace". Pressing Back or Forward triggers a third kind, a restoration
visit, which Turbo keeps for itself: it renders a cached copy of the page when it can, restores the saved scroll
position, and cannot be cancelled. While a visit is in flight, Turbo shows a thin progress bar after 500 ms and sets
aria-busy="true" on the <html> element until it completes.
Key idea
A Turbo Drive visit swaps the response body into the page and merges the head, so unchanged assets are never re-parsed; an advance visit adds a history entry, a replace visit overwrites one, and Back and Forward restore a cached page.
Checkpoint
Check: What Turbo Drive does to a page
The statuses your forms must return
Turbo handles a form submission much like a link click, except that a form can send a stateful POST, PATCH or
DELETE. That changes what the server must answer. After a successful non-GET request, Turbo expects a 303 See
Other redirect, which it follows with a fresh GET so the PATCH is never repeated. When a save fails validation,
the server re-renders the form with 422 Unprocessable Content, and Turbo shows that response in place.
| The form did | The server returns | Why |
|---|---|---|
A GET that renders a page |
200 OK | Normal navigation; Turbo swaps the body |
A POST/PATCH/DELETE that succeeds |
303 See Other, with a Location |
Turbo follows it with a GET, so a reload never resubmits |
A POST/PATCH/DELETE that fails validation |
422 Unprocessable Content | Turbo renders the returned form, with its errors, in place |
| A request that hits a server error | 500 Internal Server Error | Turbo renders it, so a “something went wrong” page shows |
Turbo refuses to render a plain 200 from a non-GET on purpose: browsers warn before reloading a POST, and Turbo
cannot replicate that dialogue, so it insists on the 303 that lands the visitor on a safe-to-reload GET URL. The
HTTP status codes reference lists each code and its meaning.
The inline edit below runs these statuses for real: an integration test posts a blank name and a valid name to a controller and prints what Turbo would receive.
# Inline editing inside a Turbo Frame, run as an integration test on the fixture
# app: db:prepare builds the development database and dumps the schema, then the
# test boots Rails, makes the three requests and prints what Turbo would receive.
require_relative "session"
Session.run(<<~SH, masks: [[/-> \d+\.\d+s/, "-> …s"]])
cd ../kaksha
bin/rails db:prepare
bin/rails test test/integration/inline_batch_edit_test.rb
SH Output
$ cd ../kaksha $ bin/rails db:prepare == 20260101000001 CreateBatches: migrating ==================================== -- create_table(:batches) -> …s == 20260101000001 CreateBatches: migrated (…s) =========================== == 20260101000001 CreateBatches: migrating ==================================== -- create_table(:batches) -> …s == 20260101000001 CreateBatches: migrated (…s) =========================== $ bin/rails test test/integration/inline_batch_edit_test.rb Running 1 tests in a single process (parallelization threshold is 50) Run options: --seed … # Running: GET /batches/1/edit -> 200 the frame Turbo swaps into the row: <turbo-frame id="batch_1"> <form action="/batches/1" accept-charset="UTF-8" method="post"><input type="hidden" name="_method" value="patch"> <label for="batch_name">Name</label> <input type="text" value="Class 10 Science" name="batch[name]" id="batch_name"> <label for="batch_teacher">Teacher</label> <input type="text" value="Meera Nair" name="batch[teacher]" id="batch_teacher"> <input type="submit" name="commit" value="Save" data-disable-with="Save"> </form></turbo-frame> PATCH with a blank name -> 422 Unprocessable Content (Turbo re-renders the frame) PATCH with a valid name -> 303 See Other, Location http://www.example.com/batches . Finished in …s, … runs/s, … assertions/s. 1 runs, 7 assertions, 0 failures, 0 errors, 0 skips
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby inline_edit_session.rb
The blank name came back as 422 Unprocessable Content and the valid name as 303 See Other, exactly as the table promises. The controller that produces them is three lines of ordinary Rails:
class BatchesController < ApplicationController
# The list page. Each row is its own Turbo Frame, so editing one row
# leaves every other row untouched.
def index
@batches = Batch.order(:id)
end
# The detail page, which carries a lazy attendance frame.
def show
@batch = Batch.find(params[:id])
end
# The form that replaces one row's frame when "Edit" is clicked.
def edit
@batch = Batch.find(params[:id])
end
def update
@batch = Batch.find(params[:id])
if @batch.update(batch_params)
# A non-GET redirect Turbo follows must be 303, so the browser issues
# the next request as a GET rather than repeating the PATCH.
redirect_to batches_path, status: :see_other
else
# A 4xx tells Turbo to render the returned frame in place, so the form
# comes back with its error messages instead of navigating away.
render :edit, status: :unprocessable_content
end
end
# The lazy frame's own request: it renders only the attendance figures.
def attendance
@batch = Batch.find(params[:id])
end
private
def batch_params
params.expect(batch: [:name, :teacher])
end
end Checkpoint
Check: The statuses your forms must return
Telling Turbo to step aside, reload assets or prefetch
Sometimes you want the browser, not Turbo, to handle a link, and sometimes you want Turbo to work harder. Three attributes cover almost every case:
| Attribute | Put it on | Effect |
|---|---|---|
data-turbo="false" |
A link, form, or any ancestor | The browser handles it as a normal, full navigation; data-turbo="true" re-enables Turbo inside an opted-out region |
data-turbo-track="reload" |
A <link> or <script> in the head |
A full reload when that asset’s URL changes, so new CSS or JavaScript is not layered on the old copy |
data-turbo-prefetch="false" |
A link or ancestor | Turns off hover prefetch for that element |
Asset tracking needs a version in the URL. Rails adds one for you: the generated layout writes stylesheet_link_tag :app, "data-turbo-track": "reload", and Propshaft gives the file a content digest, so a deploy changes the digest, the
merged head shows a different URL, and Drive does one full reload to pick up the change cleanly.
The last attribute turns off a feature that is on by default. Since Turbo 8, a link prefetch: Turbo loading a link's page in the background when the pointer rests on it, before the click, so the navigation feels instant; on by default since Turbo 8.
on hover: after the pointer rests on it briefly (about 100 ms, so a passing cursor does not trigger it), Turbo loads the
page in the background, so part of the wait is over before the click lands. Switch it off per element with
data-turbo-prefetch="false", or page-wide with a <meta name="turbo-prefetch" content="false">, for links to pages
that are expensive to render.
Checkpoint
Check: Opting out, asset tracking and prefetch
Turbo Frames scope navigation to one region
Drive swaps the whole page. A Turbo Frame: A turbo-frame element with a unique id that captures the links and forms inside it, so a response updates only that region of the page and leaves the rest untouched. swaps one region of it. Wrap a segment in a
<turbo-frame> with a unique id, and every link and form inside it is captured: the response is fetched in the
background, the <turbo-frame> with the matching id is extracted from it, and only that frame’s contents are
replaced. The rest of the page, its scroll and its focus elsewhere, stay exactly as they were.
That is what makes inline editing cheap. Each batch row is its own frame, named with dom_id(batch), and the “Edit”
link lives inside it. Step through one row as it becomes a form and returns:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Click Edit in the frame. The row sits inside a turbo-frame with id batch_1. The visitor clicks the Edit link, which lives inside that frame.
- Turbo fetches the form. Because the link is inside a frame, Turbo scopes the request to it. It fetches the edit page in the background rather than leaving the page.
- Server returns a matching frame. Rails renders a turbo-frame with the same id, batch_1, this time holding the edit form. Only that frame is read from the response.
- Swap the form into the row. Turbo replaces the row's contents with the form. Every other row, and the rest of the page, is untouched, so scroll and focus elsewhere are kept.
- Edit and submit. The visitor changes the name and submits the form, which also lives inside the frame, so its submission stays scoped to batch_1.
- Turbo sends the change. Turbo submits the form as a PATCH request, again in the background and scoped to the frame.
- An invalid name stays 422. If the name is blank, the server re-renders the frame with the error messages and status 422, and Turbo shows the form again in place.
- A valid name redirects 303. If the name is valid, the server answers 303 and Turbo follows it, fetching the list and rendering the updated row back in the frame.
In Rails, the turbo_frame_tag helper from turbo-rails writes the frame, and the edit response reuses the same id,
this time holding the form:
<%# Each row is wrapped in its own Turbo Frame, named with dom_id(batch). %>
<%# A link or form inside the frame navigates only this frame, not the page. %>
<%= turbo_frame_tag dom_id(batch) do %>
<div class="batch-row">
<span class="name"><%= batch.name %></span>
<span class="teacher"><%= batch.teacher %></span>
<%= link_to "Edit", edit_batch_path(batch) %>
</div>
<% end %> <%# The response reuses the SAME frame id as the row, so Turbo swaps the %>
<%# row's display for this form in place. %>
<%= turbo_frame_tag dom_id(@batch) do %>
<%= form_with model: @batch do |form| %>
<% if @batch.errors.any? %>
<ul class="errors">
<% @batch.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
<%= form.label :name %>
<%= form.text_field :name %>
<%= form.label :teacher %>
<%= form.text_field :teacher %>
<%= form.submit "Save" %>
<% end %>
<% end %> An integration test drives the whole cycle without a browser, reading the frame that Turbo would swap in and checking the 422 and 303 along the way:
require "test_helper"
# Inline editing inside a Turbo Frame, proved by a real request-response cycle:
# the frame comes back with a form, an invalid save answers 422, and a valid
# save answers 303. No browser is involved; the integration test is the client.
class InlineBatchEditTest < ActionDispatch::IntegrationTest
setup { @batch = batches(:science) }
test "a row's frame returns a form, errors stay 422, a save redirects 303" do
frame = "turbo-frame#batch_#{@batch.id}"
get edit_batch_path(@batch)
assert_response :success
assert_select "#{frame} form"
puts "GET #{edit_batch_path(@batch)} -> #{response.status}"
puts "the frame Turbo swaps into the row:"
puts css_select(frame).first.to_html.gsub(/\s+/, " ").strip
patch batch_path(@batch), params: { batch: { name: "" } }
assert_response :unprocessable_content
assert_select "#{frame} ul.errors li"
puts "PATCH with a blank name -> #{response.status} " \
"#{response.status_message} (Turbo re-renders the frame)"
patch batch_path(@batch), params: { batch: { name: "Class 10 Physics" } }
assert_response :see_other
puts "PATCH with a valid name -> #{response.status} " \
"#{response.status_message}, Location #{response.headers['Location']}"
end
end Navigation can also leave the frame. Set target="_top" on the frame, or data-turbo-frame="_top" on one link or
form, and that navigation drives the whole page instead; name another frame’s id to drive that frame. And
data-turbo-action="advance" on a frame promotes its navigation to a full visit, so paging through a list inside a
frame updates the address bar and history too.
A frame request whose response has no matching frame
If the response is missing a <turbo-frame> with the frame’s id, Turbo writes an error into the frame and throws.
When a request should genuinely leave the frame, such as an expired session redirecting to a login page, mark that
page with <meta name="turbo-visit-control" content="reload"> (the turbo_page_requires_reload helper) so it always
becomes a full-page navigation.
Checkpoint
Check: How a Turbo Frame scopes navigation
Lazy frames defer work until a panel is shown
A frame with a src attribute eager-loads: it fetches that URL as soon as the frame appears on the page. Add
loading="lazy" and the fetch waits until the frame becomes visible, exactly like loading="lazy" on an <img>. It is
the right tool for content behind a modal, below the fold or inside a closed disclosure.
Kaksha’s batch page ships a placeholder for attendance and loads the real figures only when that frame is shown:
<h1><%= @batch.name %></h1>
<%# A lazy frame: Rails ships the placeholder with the page, and the browser %>
<%# fetches src only when the frame scrolls into view. %>
<%= turbo_frame_tag dom_id(@batch, :attendance),
src: attendance_batch_path(@batch),
loading: :lazy do %>
<p>Loading attendance…</p>
<% end %> <%# The frame's own request answers with a matching frame id, so Turbo drops %>
<%# these figures into the placeholder above. %>
<%= turbo_frame_tag dom_id(@batch, :attendance) do %>
<p><%= @batch.present_count %> of <%= @batch.total_count %> present
(<%= @batch.attendance_percent %>%)</p>
<% end %> The lazy frame makes two trips: the page carries the placeholder and the frame’s src, then a second request loads the
frame’s own body. An integration test makes exactly those two requests:
# A lazy frame fetched as its own request, run as an integration test: the page
# ships a placeholder, then a second request loads the frame's own body.
require_relative "session"
Session.run(<<~SH, masks: [[/-> \d+\.\d+s/, "-> …s"]])
cd ../kaksha
bin/rails db:prepare
bin/rails test test/integration/lazy_attendance_frame_test.rb
SH Output
$ cd ../kaksha $ bin/rails db:prepare == 20260101000001 CreateBatches: migrating ==================================== -- create_table(:batches) -> …s == 20260101000001 CreateBatches: migrated (…s) =========================== == 20260101000001 CreateBatches: migrating ==================================== -- create_table(:batches) -> …s == 20260101000001 CreateBatches: migrated (…s) =========================== $ bin/rails test test/integration/lazy_attendance_frame_test.rb Running 1 tests in a single process (parallelization threshold is 50) Run options: --seed … # Running: GET /batches/1 (the page) ships: <turbo-frame loading="lazy" id="attendance_batch_1" src="/batches/1/attendance"> <p>Loading attendance…</p> </turbo-frame> GET /batches/1/attendance (the frame's own request) returns: <turbo-frame id="attendance_batch_1"> <p>27 of 30 present (90%)</p> </turbo-frame> . Finished in …s, … runs/s, … assertions/s. 1 runs, 6 assertions, 0 failures, 0 errors, 0 skips
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby lazy_frame_session.rb
The page shipped the frame with loading="lazy" and its src; the second request returned a matching frame with
“27 of 30 present (90%)”. While the frame fetches, Turbo sets aria-busy="true" on it and removes it when done. A lazy
frame whose panel stays closed never costs its request at all. Frames also help caching: put the part of a page that
differs per user in its own frame, and the rest can come from one shared cache entry. Frames that are visible on load
are another matter: each is one more request, and the page can shift as their content arrives, so keep those few.
Checkpoint
Check: Lazy frames and their cost
Page refreshes that morph instead of replacing
A common flow is to submit a form and be redirected back to the same page, which then refreshes itself. By default that
replaces the whole <body> and jumps the scroll to the top. morphing: A page-refresh method in which Turbo updates only the DOM nodes that changed, keeping the rest of the page in place, instead of replacing the whole body. does better: it
compares the new DOM with the current one and updates only the nodes that changed, leaving the rest in place.
| Refresh method | What happens | Scroll |
|---|---|---|
replace (the default) |
The whole body is replaced with the new one | Resets to the top, unless preserved |
morph |
Only the DOM nodes that differ are updated, keeping the rest | preserve keeps the vertical and horizontal scroll |
You opt in with two meta tags, <meta name="turbo-refresh-method" content="morph"> and <meta name="turbo-refresh-scroll" content="preserve">. The turbo-rails helper turbo_refreshes_with(method: :morph, scroll: :preserve) writes both for you. Flag any element that must survive the morph untouched, such as an open popover, with
data-turbo-permanent. To push a change to several parts of a page at once, or to update other visitors’ screens
live, Turbo Streams take over; the next lesson, Turbo Streams and model
broadcasts, covers them.
Checkpoint
Check: Page refreshes with morphing
Frame swaps need focus and an announcement from you
A frame swap is quiet. Turbo toggles aria-busy while it fetches and announces nothing when the new content lands.
It moves keyboard focus only when that content holds a focusable element marked autofocus; otherwise focus stays
where it was, which can be a link that no longer exists. A sighted visitor sees the row become a form; a
screen-reader user is told nothing.
| After a frame swap | What Turbo does | What you must add |
|---|---|---|
| Keyboard focus | Focuses the first autofocus element of the new content, if any |
Mark the form’s first field autofocus; move focus back to the row after a save |
| Screen-reader announcement | Nothing | Announce a short message in an aria-live="polite" region |
| Loading state | Sets aria-busy="true", then removes it |
Nothing; this one is handled |
So the inline edit is not finished when the 303 lands. Give the edit form’s first field autofocus, so focus lands in
the form when it arrives. When the row returns, move focus to it and write a brief message
such as “Batch saved” into a polite live region, so keyboard and screen-reader users get the same feedback a sighted
user gets from the row changing in place. A small Stimulus controller is the usual home for that work, which the
Stimulus controllers lesson builds.
Relying on the visible change alone
Signalling a save only by recolouring the row, or by a toast that vanishes after a second, leaves out anyone who cannot see it or who reads at their own pace. Focus management plus a polite live region reaches everyone.
Checkpoint
Check: The accessibility duty of frames
Practise
These questions come from this lesson’s runs and tables: the Drive visit, the statuses the inline edit returned, the frame cycle, the lazy attendance frame, and the accessibility duty. Pick an answer and its explanation opens at once:
Practice that adapts to you
Practise this lesson
12 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: a form in a Rails app submits with Turbo. What status should the controller return on a successful save, and what on a validation error, and why those two?
On a successful non-GET save, return a 303 See Other redirect; on a validation error, re-render the form with
422 Unprocessable Content. Turbo follows a 303 with a fresh GET, so a reload never resubmits the POST or
PATCH, which is the behaviour a browser’s own “resubmit this form?” dialogue would otherwise guard. Turbo renders 4xx
and 5xx responses in place, so a 422 brings the form back with its error messages, and a 500 shows a failure page. A
plain 200 from a non-GET is the one thing Turbo will not navigate to, because it has no safe way to handle a reload of
it. Rails 8.1 scaffolds already use status: :see_other and status: :unprocessable_content for exactly this.
The module’s question bank builds two longer cases on this lesson’s runs: the statuses a frame form must return and what breaks without them, and how to keep an inline edit accessible.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
An inline edit inside a Turbo Frame posts fine in the browser, but after a successful save the page does not update and after an invalid save the errors never show. The controller renders with status 200 both times. Explain what Turbo does with those responses and how you would fix the controller.
-
Mid to senior
You ship inline editing with Turbo Frames: a row becomes a form and returns on save. A screen-reader user reports they cannot tell when a row changed, and a keyboard user loses their place. What is happening, and how do you make the feature accessible?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does Turbo Drive do with a link click?
It turns the click into a visit: it fetches the page in the background, swaps in the response's body and merges its head, so assets that did not change are never parsed again.
-
How do advance and replace visits treat the browser's history?
An advance visit adds a history entry and a replace visit overwrites the current one. Back and Forward restore a page from Turbo's cache.
-
Which answers does a form submitted through Turbo need, and which answer will Turbo not follow?
A 303 redirect after a successful save, which Turbo follows with a fresh GET so a reload never resubmits, and 422 when validation fails, which Turbo renders in place. A plain 200 to a non-GET submission is the one answer it will not navigate to.
-
What do data-turbo="false" and data-turbo-track="reload" do?
data-turbo="false" hands a link or form back to the browser. data-turbo-track="reload" on a versioned asset forces a full page load when that asset changes. Since Turbo 8, links also prefetch on hover by default.
-
A link inside a Turbo Frame is clicked. What must the response contain?
A turbo-frame with the same id; Turbo takes that frame from the response and leaves the rest of the page alone. target="_top" sends the navigation to the whole page, and data-turbo-action="advance" also updates the URL.
-
When does a frame with src fetch its content, and what does loading="lazy" change?
Without it the frame loads together with the page. With loading="lazy" it waits until the frame becomes visible, so a panel nobody opens never costs its request.
-
What does a morphing page refresh keep, and what must you add yourself for inline frame updates?
Morphing changes only the nodes that differ, and the turbo-refresh-scroll setting preserve keeps the scroll position. A frame swap announces nothing and moves focus only to an autofocus element, so inline updates need focus handling and a polite live region.
Key takeaways
- Turbo Drive turns a link click or form submission into a visit: it fetches in the background, swaps the response
<body>in and merges the<head>, so unchanged assets are never re-parsed. - Forms must answer Turbo with a 303 redirect after a successful non-GET and a 422 when validation fails; a plain 200 from a non-GET is the one response Turbo will not navigate to.
data-turbo="false"hands a link back to the browser,data-turbo-track="reload"forces a full reload when a versioned asset changes, and links prefetch on hover by default since Turbo 8.- A Turbo Frame scopes links and forms to one region; the response must contain a
<turbo-frame>with the matchingid, andtarget="_top"ordata-turbo-action="advance"change that scope or update the URL. - A frame with
srceager-loads; addingloading="lazy"defers the fetch until the frame is visible, so a panel that stays hidden never costs its request. - A page refresh can morph to update only changed nodes and preserve scroll, but a frame swap announces nothing and
moves focus only to an
autofocuselement, so inline updates need focus management and a polite live region.
Terms in this lesson
- morphing
- A page-refresh method in which Turbo updates only the DOM nodes that changed, keeping the rest of the page in place, instead of replacing the whole body.
- prefetch
- Turbo loading a link's page in the background when the pointer rests on it, before the click, so the navigation feels instant; on by default since Turbo 8.
- Turbo Drive
- The part of Turbo that intercepts link clicks and form submissions, fetches the page in the background, and updates it by swapping the body and merging the head instead of a full reload.
- Turbo Frame
- A turbo-frame element with a unique id that captures the links and forms inside it, so a response updates only that region of the page and leaves the rest untouched.
References
- Turbo Handbook: Navigate with Turbo Drive (37signals)
- Turbo Handbook: Decompose with Turbo Frames (37signals)
- Turbo Handbook: Smooth page refreshes with morphing (37signals)
- turbo-rails DriveHelper source (v2.0.23) (37signals)
- Working with JavaScript in Rails (Ruby on Rails)
- WAI-ARIA Authoring Practices: alert and status (live regions) (W3C)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress