| 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_tagwith a model — Rails generates consistent IDs (invoice_123) - Break out of frames with
data-turbo-frame="_top"for full-page navigation - Lazy load with
srcandloading: "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:
| Action | Description |
|---|---|
append |
Add to end of target container |
prepend |
Add to beginning of target container |
replace |
Replace the entire target element |
update |
Replace only the content (innerHTML) |
remove |
Remove the target element |
before |
Insert before the target |
after |
Insert after the target |
morph |
Morph the target element (intelligent diff) |
refresh |
Trigger 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
| Scenario | Solution |
|---|---|
| Standard navigation | Turbo Drive (automatic) |
| Edit form inline | Turbo Frame |
| Load section lazily | Turbo Frame with src |
| Add item to list | Turbo Stream append/prepend |
| Update multiple areas | Turbo Stream |
| Real-time via WebSocket | Turbo Stream over ActionCable |
| Delete from list | Turbo Stream remove |
| Modal/slideout | Turbo Frame targeting a container |
| Preserve scroll on refresh | Morphing with turbo-refresh |
| Smooth inline update | Turbo Stream morph |
| Update without losing focus | Morphing |
Key Principles
- Progressive Enhancement — Start with Turbo Drive, add Frames for scoped updates, then Streams for complex interactions
- Minimize JavaScript — Use Stimulus only when you need client-side behavior that can't be achieved with Turbo
- Semantic IDs — Use model-based IDs (
dom_id(@invoice)) for reliable targeting - Graceful Degradation — Always have an
htmlformat fallback for non-Turbo requests - Keep Frames Small — Smaller frames = faster updates and easier maintenance
- Use Morphing for Polish — Enable morphing when smooth transitions matter (forms, live updates)