hotwire-turbo

Best practices for using Hotwire Turbo to create reactive applications

$ Installieren

git clone https://github.com/sandnap/easy_notes /tmp/easy_notes && cp -r /tmp/easy_notes/.claude/skills/hotwire-turbo ~/.claude/skills/easy_notes

// tip: Run this command in your terminal to install the skill


name: hotwire-turbo description: Best practices for using Hotwire Turbo to create reactive applications

Hotwire Best Practices for Reactive Applications

Rule updated on 12/15/2025 to Turbo version 8.0.18.

Hotwire consists of three main components, each suited for different use cases. Here's when to use each.

For full reference see https://turbo.hotwired.dev/

Turbo Drive (Default Page Navigation)

When to use:

  • Standard page-to-page navigation (it's on by default)
  • Full page updates where you want faster transitions without a full browser reload
  • Simple CRUD operations where you're redirecting after an action

How it works: Intercepts link clicks and form submissions, fetches the new page via AJAX, and swaps the <body> content while keeping the <head> intact.

Best practices:

  • It's automatic—you get it for free in Rails 8
  • Use data-turbo="false" to disable for specific links/forms (e.g., file downloads, external links)
  • Use data-turbo-method="delete" for non-GET requests from links
<%= link_to "Delete", invoice_path(@invoice), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>

Turbo 8 Morphing (Smooth Page Refreshes)

When to use:

  • Form submissions where you want to preserve scroll position
  • Updates that should feel seamless without visible "flash"
  • Pages with user input that shouldn't be lost during refresh
  • Maintaining focus state and CSS transitions during updates

How it works: Instead of replacing the entire <body>, morphing intelligently diffs the current DOM against the new HTML and applies only the necessary changes. This preserves:

  • Scroll position
  • Form input values
  • Focus state
  • Active CSS transitions/animations

Enabling morphing:

<%# In your layout or page head %>
<meta name="turbo-refresh-method" content="morph">

<%# Optionally preserve scroll position %>
<meta name="turbo-refresh-scroll" content="preserve">

Per-element control:

<%# Keep an element from being morphed (preserves exact state) %>
<div data-turbo-permanent id="chat-messages">
  <!-- Content preserved across page updates -->
</div>

When NOT to use morphing:

  • When you want a clear visual transition between pages
  • When the page structure changes dramatically
  • When you need to reset all page state

Turbo Frames (Partial Page Updates)

When to use:

  • Inline editing (edit-in-place forms)
  • Lazy loading content sections
  • Modal dialogs or slideovers
  • Tabbed interfaces
  • Pagination within a section
  • Any time you want to update a specific region without touching the rest

How it works: Wraps a section of the page in a <turbo-frame> tag. When a link or form inside the frame is activated, only that frame's content is replaced.

Best practices:

<!-- index.html.erb -->
<%= turbo_frame_tag @invoice do %>
  <div class="invoice-row">
    <%= @invoice.number %>
    <%= link_to "Edit", edit_invoice_path(@invoice) %>
  </div>
<% end %>

<!-- edit.html.erb -->
<%= turbo_frame_tag @invoice do %>
  <%= form_with model: @invoice do |f| %>
    <!-- form fields -->
  <% end %>
<% end %>
  • Use turbo_frame_tag with a model — Rails generates consistent IDs (invoice_123)
  • Break out of frames with data-turbo-frame="_top" for full-page navigation
  • Lazy load with src and loading: "lazy":
    <%= turbo_frame_tag "comments", src: comments_path, loading: "lazy" do %>
      <p>Loading comments...</p>
    <% end %>
    
  • Target other frames with data-turbo-frame="frame_id"

Turbo Streams (Real-Time DOM Manipulation)

When to use:

  • Updating multiple parts of the page from a single action
  • Real-time updates via WebSockets (ActionCable)
  • Adding/removing items from lists without full refresh
  • Flash messages after form submissions
  • Counter/badge updates
  • Any scenario where you need surgical DOM updates

How it works: Returns <turbo-stream> elements that specify actions (append, prepend, replace, update, remove, before, after, morph, refresh) and target DOM elements by ID.

Best practices:

# Controller
def create
  @invoice = current_user.invoices.build(invoice_params)

  respond_to do |format|
    if @invoice.save
      format.turbo_stream  # Renders create.turbo_stream.erb
      format.html { redirect_to @invoice }
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end
<!-- create.turbo_stream.erb -->
<%= turbo_stream.prepend "invoices", @invoice %>
<%= turbo_stream.update "invoice_count", Invoice.count %>
<%= turbo_stream.remove "empty_state" %>

Stream Actions:

ActionDescription
appendAdd to end of target container
prependAdd to beginning of target container
replaceReplace the entire target element
updateReplace only the content (innerHTML)
removeRemove the target element
beforeInsert before the target
afterInsert after the target
morphMorph the target element (intelligent diff)
refreshTrigger a full page refresh (optionally with morph)

Morph stream action example:

<%# Smoothly update a section without replacing it entirely %>
<%= turbo_stream.morph "invoice_#{@invoice.id}", partial: "invoices/invoice", locals: { invoice: @invoice } %>

Decision Matrix

ScenarioSolution
Standard navigationTurbo Drive (automatic)
Edit form inlineTurbo Frame
Load section lazilyTurbo Frame with src
Add item to listTurbo Stream append/prepend
Update multiple areasTurbo Stream
Real-time via WebSocketTurbo Stream over ActionCable
Delete from listTurbo Stream remove
Modal/slideoutTurbo Frame targeting a container
Preserve scroll on refreshMorphing with turbo-refresh
Smooth inline updateTurbo Stream morph
Update without losing focusMorphing

Key Principles

  1. Progressive Enhancement — Start with Turbo Drive, add Frames for scoped updates, then Streams for complex interactions
  2. Minimize JavaScript — Use Stimulus only when you need client-side behavior that can't be achieved with Turbo
  3. Semantic IDs — Use model-based IDs (dom_id(@invoice)) for reliable targeting
  4. Graceful Degradation — Always have an html format fallback for non-Turbo requests
  5. Keep Frames Small — Smaller frames = faster updates and easier maintenance
  6. Use Morphing for Polish — Enable morphing when smooth transitions matter (forms, live updates)