erb ruby 83 lines · 3 tabs

Turbo Frames: scoped navigation inside a sidebar

Shared by codesnips Jan 2026
3 tabs
<div class="settings-layout">
  <nav class="settings-sidebar">
    <h2>Settings</h2>
    <ul>
      <% settings_sections.each do |section| %>
        <li class="<%= "active" if current_page?(section.path) %>">
          <%= link_to section.label, section.path,
                data: { turbo_frame: "settings_content" } %>
        </li>
      <% end %>

      <li>
        <%# Escapes the frame and reloads the whole document %>
        <%= link_to "Back to app", root_path,
              data: { turbo_frame: "_top" } %>
      </li>
    </ul>
  </nav>

  <main class="settings-content">
    <%= turbo_frame_tag :settings_content, data: { turbo_action: "advance" } do %>
      <%= render "settings/profile", user: @user %>
    <% end %>
  </main>
</div>
3 files · erb, ruby Explain with highlit

This snippet shows how Turbo Frames confine navigation to a region of the page so that clicking links in a sidebar swaps only the main content area, without a full-page reload. The pattern gives an app SPA-like feel with zero custom JavaScript: the browser still issues normal HTTP requests, but Turbo intercepts the response and grafts only the matching <turbo-frame> into the DOM.

The key idea in settings/index.html.erb is that the whole layout is split into a persistent sidebar and a single turbo_frame_tag :settings_content that wraps the mutable content. Because the frame has a stable DOM id, any navigation targeting it replaces its innards while everything outside the frame — the sidebar, headers, scroll position — stays put. Each sidebar link sets data: { turbo_frame: "settings_content" }, which tells Turbo to route that link's response into the frame even though the link itself lives outside it. This decoupling is what makes the sidebar act as a router for the content pane.

In settings/profile.html.erb, the destination page wraps its body in a matching turbo_frame_tag :settings_content. Turbo matches frames by id: it pulls just that element out of the full HTML response and discards the rest, so the same template still renders correctly on a direct visit or hard refresh. The data-turbo-action="advance" attribute on the frame pushes a new history entry, keeping the URL bar and back button honest even though no full navigation happened.

SettingsController looks completely ordinary — it renders normal full templates and knows nothing about frames. That is the point: framing is a view-layer concern, so the controller stays reusable for API-style or full-page rendering. request.headers["Turbo-Frame"] is available if a lighter layout is desired for framed requests, shown here by skipping the application layout.

The main pitfalls are id mismatches (a response without the expected frame id yields an empty pane) and links that should escape the frame, which need data: { turbo_frame: "_top" }. This approach fits dashboards, settings screens, and master-detail UIs where most of the chrome is stable and only one panel changes.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Turbo Frames: scoped navigation inside a sidebar — share card
Link copied