Claude-skill-registry hotwire-turbo
Best practices for using Hotwire Turbo to create reactive applications
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/hotwire-turbo" ~/.claude/skills/majiayu000-claude-skill-registry-hotwire-turbo && rm -rf "$T"
manifest:
skills/data/hotwire-turbo/SKILL.mdsource content
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
to disable for specific links/forms (e.g., file downloads, external links)data-turbo="false" - Use
for non-GET requests from linksdata-turbo-method="delete"
<%= 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
with a model — Rails generates consistent IDs (turbo_frame_tag
)invoice_123 - Break out of frames with
for full-page navigationdata-turbo-frame="_top" - Lazy load with
andsrc
: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:
| Action | Description |
|---|---|
| Add to end of target container |
| Add to beginning of target container |
| Replace the entire target element |
| Replace only the content (innerHTML) |
| Remove the target element |
| Insert before the target |
| Insert after the target |
| Morph the target element (intelligent diff) |
| 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 |
| Add item to list | Turbo Stream |
| Update multiple areas | Turbo Stream |
| Real-time via WebSocket | Turbo Stream over ActionCable |
| Delete from list | Turbo Stream |
| Modal/slideout | Turbo Frame targeting a container |
| Preserve scroll on refresh | Morphing with |
| Smooth inline update | Turbo Stream |
| 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 (
) for reliable targetingdom_id(@invoice) - Graceful Degradation — Always have an
format fallback for non-Turbo requestshtml - Keep Frames Small — Smaller frames = faster updates and easier maintenance
- Use Morphing for Polish — Enable morphing when smooth transitions matter (forms, live updates)