# Hotwire Turbo

> Best practices for using Hotwire Turbo to create reactive applications Use when this capability is needed.

- Skill: `tomevault-io/hotwire-turbo` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/hotwire-turbo`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/hotwire-turbo/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/hotwire-turbo

---


# 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 `<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

```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 `<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:**

```erb
<%# 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:**

```erb
<%# 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:**

```erb
<!-- 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"`:
  ```erb
  <%= 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:**

```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
<!-- 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:**

```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)

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/sandnap) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-11 -->

