
hotwire-turbo
by sandnap
SKILL.md
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)
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon