Turbo Frames: Update Just Part of Your Page

Turbo Drive updates the whole page. But what if you only want to update part of it? A sidebar. A modal. A comment thread. A paginated list.

That's what Turbo Frames are for. They let you carve your page into independent regions that navigate without affecting anything else.

What Turbo Frames Do


A Turbo Frame is a section of your page that can navigate independently. Wrap content in a <turbo-frame> tag, and links inside it only update that frame.

<turbo-frame id="messages">
  <h2>Messages</h2>
  <a href="/messages/1">Message 1</a>
  <a href="/messages/2">Message 2</a>
</turbo-frame>

Click a link inside that frame:

1. Turbo fetches /messages/1
2. Turbo finds the matching <turbo-frame id="messages"> in the response
3. Turbo swaps just that frame's content
4. Everything outside the frame stays untouched

Think of it like an iframe, but better. No cross-origin restrictions. Shares your CSS and JavaScript. Part of the same DOM.

A Simple Example


Here's an index page with a message list inside a frame:

<header>My App</header>

<turbo-frame id="messages">
  <h2>Your Messages</h2>
  <ul>
    <li><a href="/messages/1">Hey, how are you?</a></li>
    <li><a href="/messages/2">Meeting at 3pm</a></li>
  </ul>
</turbo-frame>

<footer>© 2025</footer>

And the detail page at /messages/1:

<header>My App</header>

<turbo-frame id="messages">
  <h2>Message from Alice</h2>
  <p>Hey, how are you? Want to grab coffee?</p>
  <a href="/messages">← Back</a>
</turbo-frame>

<footer>© 2025</footer>

Click the first message. Turbo fetches the detail page, extracts the matching frame, swaps it in. Header and footer never change. Your server still renders complete HTML pages—Turbo just extracts the part it needs.

This means your routes work with or without JavaScript. Direct links work. Search engines can crawl everything. Progressive enhancement done right.

The ID Matching Rule


The frame's id in the response must match the frame that initiated the request. If they don't match, Turbo shows an error.

<!-- Page A -->
<turbo-frame id="comments">
  <a href="/comments/new">Add Comment</a>
</turbo-frame>

<!-- /comments/new must have -->
<turbo-frame id="comments">
  <form action="/comments" method="post">...</form>
</turbo-frame>

This is intentional. It prevents accidentally replacing the wrong part of your page.

Lazy Loading


Frames can load content on demand. Add a src attribute and Turbo fetches it automatically:

<turbo-frame id="comments" src="/posts/1/comments">
  <p>Loading comments...</p>
</turbo-frame>

By default that loads immediately (eager loading). Add loading="lazy" to defer until the frame scrolls into view:

<turbo-frame id="comments" src="/posts/1/comments" loading="lazy">
  <p>Loading comments...</p>
</turbo-frame>

Use cases: comments below articles, analytics dashboards, related products below the fold. Anything expensive that users might not scroll to.

Breaking Out of Frames


Sometimes a link inside a frame should navigate the whole page. Use data-turbo-frame="_top":

<turbo-frame id="modal">
  <form action="/profile" method="post">
    <input name="name" value="Alice">
    <button>Save</button>
  </form>
  
  <!-- This navigates the whole page -->
  <a href="/dashboard" data-turbo-frame="_top">Cancel</a>
</turbo-frame>

Targeting options:

data-turbo-frame="_top" — Navigate the whole page
data-turbo-frame="_self" — Navigate this frame (default)
data-turbo-frame="other-id" — Navigate a different frame

Cross-Frame Navigation


You can target a different frame entirely. Classic sidebar + main content pattern:

<turbo-frame id="sidebar">
  <nav>
    <a href="/dashboard" data-turbo-frame="main">Dashboard</a>
    <a href="/analytics" data-turbo-frame="main">Analytics</a>
    <a href="/settings" data-turbo-frame="main">Settings</a>
  </nav>
</turbo-frame>

<turbo-frame id="main">
  <!-- Content loads here -->
</turbo-frame>

Clicking sidebar links updates the main frame, not the sidebar.

Updating the URL


By default, frame navigation does NOT update the browser URL. The URL stays the same while the frame content changes.

If you want the URL to update (for bookmarking, sharing, back button), add data-turbo-action="advance":

<turbo-frame id="products">
  <a href="/products?page=2" data-turbo-action="advance">Next Page</a>
</turbo-frame>

Now clicking "Next Page" updates the URL to /products?page=2 and adds a history entry. Back button works.

Forms in Frames


Forms inside frames work the same way as links. Submit the form, the response updates just that frame.

Inline editing example:

<turbo-frame id="user-name">
  <p>
    <strong>Name:</strong> Alice Smith
    <a href="/profile/edit">Edit</a>
  </p>
</turbo-frame>

Click "Edit", Turbo fetches the form:

<turbo-frame id="user-name">
  <form action="/profile" method="post">
    <input name="name" value="Alice Smith">
    <button>Save</button>
    <a href="/profile">Cancel</a>
  </form>
</turbo-frame>

Submit the form, server responds with updated frame:

<turbo-frame id="user-name">
  <p>
    <strong>Name:</strong> Alice Johnson
    <a href="/profile/edit">Edit</a>
  </p>
</turbo-frame>

Inline editing with zero JavaScript. If validation fails, just render the form again with errors—Turbo swaps it in.

Real-World Patterns


Pagination:

<turbo-frame id="products">
  <div class="product">Product 1</div>
  <div class="product">Product 2</div>
  
  <nav>
    <a href="/products?page=1" data-turbo-action="advance">1</a>
    <a href="/products?page=2" data-turbo-action="advance">2</a>
  </nav>
</turbo-frame>

Page numbers update only the product list. Filters, header, footer stay put.

Modals:

<!-- Trigger -->
<a href="/posts/new" data-turbo-frame="modal">New Post</a>

<!-- Empty frame that receives modal content -->
<turbo-frame id="modal"></turbo-frame>

Server renders the form inside a matching frame. Form appears in the modal. Submit creates the post.

Tabs:

<div class="tabs">
  <a href="/settings/profile" data-turbo-frame="tab-content">Profile</a>
  <a href="/settings/security" data-turbo-frame="tab-content">Security</a>
</div>

<turbo-frame id="tab-content">
  <!-- Tab content loads here -->
</turbo-frame>

Search:

<form action="/search" method="get" data-turbo-frame="results">
  <input name="q" type="search" placeholder="Search...">
  <button>Search</button>
</form>

<turbo-frame id="results">
  <!-- Results appear here -->
</turbo-frame>

Permanent Elements


Some things shouldn't reset when a frame updates. Audio players, for example. Mark them with data-turbo-permanent:

<div id="audio-player" data-turbo-permanent>
  <audio src="/stream.mp3" autoplay></audio>
</div>

Element must have an id and exist on both pages.

Disabling Frames


Add disabled to make links and forms inside the frame use normal navigation:

<turbo-frame id="messages" disabled>
  <!-- Normal navigation, not scoped to frame -->
</turbo-frame>

Common Mistakes


"Response has no matching turbo-frame": Your server's response doesn't include a frame with the expected ID. Make sure the response HTML has <turbo-frame id="..."> with the matching ID.

Link navigates whole page instead of frame: Either the link has data-turbo-frame="_top", or the link is outside the frame. Move it inside, or remove the _top target.

Form submission navigates whole page: Server is redirecting to a URL that doesn't have the frame. Either render the updated frame directly (no redirect), or redirect to a page that includes the frame.

When to Use Frames vs Drive


Use Turbo Drive when the entire page should change. Standard page-to-page navigation.

Use Turbo Frames when only part of the page should update. Modals, tabs, pagination, sidebars, inline editing.

Most apps use both. Drive handles primary navigation. Frames handle specific interactive regions.

The Bottom Line


Turbo Frames decompose your page into independent navigation scopes. Wrap content in <turbo-frame id="...">, and links inside it only update that region.

Key points:

1. IDs must match between request and response
2. Use src for lazy loading
3. Use data-turbo-frame to target different frames
4. Use data-turbo-action="advance" if you want URL updates

Complex, interactive UIs with simple HTML. No JSON APIs. No client-side state management.

But what about real-time updates? What if the server needs to push changes to the page, not just respond to user actions? That's where Turbo Streams come in. Next post.

References