---
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/](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 `
` content while keeping the `` 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
```erb
<%= 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 ``, 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:**
```erb
<%# In your layout or page head %>
<%# Optionally preserve scroll position %>
```
**Per-element control:**
```erb
<%# Keep an element from being morphed (preserves exact state) %>
```
**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 `` tag. When a link or form inside the frame is activated, only that frame's content is replaced.
**Best practices:**
```erb
<%= turbo_frame_tag @invoice do %>
<%= @invoice.number %>
<%= link_to "Edit", edit_invoice_path(@invoice) %>
<% end %>
<%= turbo_frame_tag @invoice do %>
<%= form_with model: @invoice do |f| %>
<% 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"`:
```erb
<%= turbo_frame_tag "comments", src: comments_path, loading: "lazy" do %>
Loading comments...
<% 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 `` elements that specify actions (`append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `morph`, `refresh`) and target DOM elements by ID.
**Best practices:**
```ruby
# 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
```
```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:**
```erb
<%# 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
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)