Reducing JS Boilerplate with Turbo Frames in Rails
Stop writing custom JavaScript for simple UI updates. Learn how to use Turbo Frames in Ruby on Rails to handle partial page updates and inline editing directly from the server.
17 Jul 2026, 01:19 UTC

The Problem: The 'Micro-Interaction' JavaScript Tax
Building a modern web interface often requires small, isolated updates—like changing a read-only record to an edit form or updating a pagination list—without refreshing the entire page. Traditionally, this required writing custom JavaScript to fetch a JSON response, manually manipulating the DOM, and managing loading states. This "JS tax" adds complexity to the codebase and creates a synchronization gap between the server-side HTML and the client-side state.
The takeaway: Turbo Frames allow you to treat sections of your page as independent request/response targets. By wrapping content in a specific HTML element, you can shift the logic of partial updates back to the server, removing the need for custom API endpoints and client-side DOM manipulation.
How Turbo Frames Intercept Navigation
A Turbo Frame is defined by the <turbo-frame> element. When a user clicks a link or submits a form inside this element, Turbo intercepts the request. Instead of a full page transition, Turbo expects the server to return a response containing a frame with a matching id.
Turbo then extracts the content of that matching frame from the response and replaces the content of the original frame on the page. The rest of the DOM remains untouched, preserving scroll position and any active state in other components.
Implementing an Inline Edit Pattern
A common use case is the "Edit in Place" pattern. Instead of navigating to a separate /users/1/edit page, the edit form appears exactly where the user's profile details were displayed.
1. The View (Read-only state)
Wrap the record in a frame. The link inside the frame triggers the update.
<!-- app/views/users/show.html.erb -->
<turbo-frame id=\"user_profile\">
<p>Name: <%= @user.name %></p>
<%= link_to \"Edit Profile\", edit_user_path(@user) >
</turbo-frame>2. The Controller
The controller remains standard Rails; it doesn't need to know it is responding to a Turbo Frame.
# app/controllers/users_controller.rb
def edit
@user = User.find(params[:id])
# Rails renders the edit view normally
end3. The Edit View
The response must wrap the form in a frame with the same ID as the original.
<!-- app/views/users/edit.html.erb -->
<turbo-frame id=\"user_profile\">
<%= form_with model: @user do |f| %>
<%= f.text_field :name %>
<%= f.submit \"Save\" %>
</form>
</turbo-frame>Diagnostic Checks and Constraints
When implementing frames, the most common failure is a "silent" failure where the page does not update. Use the browser's Network tab to verify the following:
- Matching IDs: Ensure the
idattribute of the<turbo-frame>in the response exactly matches theidon the page. - Frame Presence: If the server returns a page without the matching frame, Turbo will not update the content. Depending on the configuration, it may do nothing or trigger a full page reload.
- URL Persistence: By default, Turbo Frames do not change the browser's URL. If you need the URL to update for bookmarking purposes, add
data-turbo-action=\"advance\"to the link.
Trade-offs: When Frames Aren't Enough
Turbo Frames are designed for isolated updates. They struggle when an action in one frame needs to update another part of the page. For example, if saving a user profile in #user_profile should also update a #navigation_bar showing the user's name, a Frame alone cannot do this.
In these scenarios, you must move from Turbo Frames to Turbo Streams, which allow the server to push multiple DOM changes to different parts of the page in a single response.
Practical Verification
To verify your implementation is working as intended:
- Open the browser DevTools Network tab.
- Trigger the frame action (e.g., click \"Edit\").
- Confirm the request is made, but the browser does not perform a full page reload (the page should not \"flash\" white).
- Inspect the response body to ensure the HTML is wrapped in the correct
<turbo-frame id=\"...\">.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.