erb ruby scss 94 lines · 4 tabs

Turbo Frames: “details” panel that lazy-loads on click

Shared by codesnips Jan 2026
4 tabs
<div class="layout">
  <ul class="product-list">
    <% @products.each do |product| %>
      <li class="product-row">
        <%= link_to product.name,
              product_path(product),
              data: { turbo_frame: "details_panel" },
              class: "product-link" %>
        <span class="price"><%= number_to_currency(product.price) %></span>
      </li>
    <% end %>
  </ul>

  <%= turbo_frame_tag "details_panel", class: "details-panel" do %>
    <p class="empty-state">Select a product to see its details.</p>
  <% end %>
</div>
4 files · erb, ruby, scss Explain with highlit

This snippet demonstrates a common Hotwire pattern: a master list where clicking a row lazy-loads a detail panel over the wire, without writing any JavaScript. The technique relies on Turbo Frames, where a turbo_frame_tag acts as a scoped placeholder that Turbo only fills when it becomes activated, keeping the initial page light and the detail fetch on demand.

In products/index.html.erb the list of rows is rendered normally, but each link_to carries data: { turbo_frame: "details_panel" }. That attribute retargets the navigation: instead of replacing the whole page, Turbo intercepts the click, requests the linked URL, and swaps only the matching frame in the response into the on-page frame with the same id. An empty turbo_frame_tag "details_panel" sits alongside the list as the target, showing a placeholder prompt until a row is chosen.

The key to laziness lives in products/show.html.erb. The response wraps its content in a turbo_frame_tag "details_panel" whose id must match the frame that triggered the request; Turbo extracts exactly that fragment from the full HTML document and discards the rest. Because the frame is only requested when a link inside details_panel is clicked, the expensive detail rendering — associations, computed stats — never runs on the initial index load.

The ProductsController needs no special branching. show responds with a standard full page; Turbo's frame extraction happens client-side. The render layout: false guard is a small optimization: when the request targets a frame, skipping the application layout avoids sending the surrounding chrome that would be thrown away anyway, reducing payload size.

The main trade-off is that frame ids must stay consistent between the trigger and the response, or Turbo silently finds nothing to swap and logs a missing-frame warning. Another pitfall is that links outside the frame still navigate the whole page unless they also declare a turbo_frame. This pattern shines for detail views, inline edit forms, and tabbed content where a full SPA is overkill but a snappy partial update improves perceived performance. It degrades gracefully too: with JavaScript disabled the same links load the full show page.


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
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 2 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

Share this code

Here's the card — post it anywhere.

Turbo Frames: “details” panel that lazy-loads on click — share card
Link copied