Best use case
hotwire-turbo is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Best practices for using Hotwire Turbo to create reactive applications
Teams using hotwire-turbo should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/hotwire-turbo/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How hotwire-turbo Compares
| Feature / Agent | hotwire-turbo | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Best practices for using Hotwire Turbo to create reactive applications
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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)Related Skills
stimulus
Best practices for using Stimulus controllers to add JavaScript behavior to HTML
nextjs-turbopack
Next.js 16+ and Turbopack — incremental bundling, FS caching, dev speed, and when to use Turbopack vs webpack.
turborepo-caching
Configure Turborepo for efficient monorepo builds with local and remote caching. Use when setting up Turborepo, optimizing build pipelines, or implementing distributed caching.
turborepo
Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines, dependsOn, caching, remote cache, the "turbo" CLI, --filter, --affected, CI optimization, environment variables, internal packages, monorepo structure/best practices, and boundaries. Use when user: configures tasks/workflows/pipelines, creates packages, sets up monorepo, shares code between apps, runs changed/affected packages, debugs cache, or has apps/packages directories.
turborepo
Turborepo expert guidance. Use when setting up or optimizing monorepo builds, configuring task caching, remote caching, parallel execution, or the --affected flag for incremental CI.
turbopack
Turbopack expert guidance. Use when configuring the Next.js bundler, optimizing HMR, debugging build issues, or understanding the Turbopack vs Webpack differences.
turborepo
Turborepo configuration, caching, and pipeline optimization.
turbopack
Turbopack configuration and Next.js integration.
turborepo-caching
Configure Turborepo for efficient monorepo builds with local and remote caching. Use when setting up Turborepo, optimizing build pipelines, or implementing distributed caching.
turborepo
Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines, dependsOn, caching, remote cache, the "turbo" CLI, --filter, --affected, CI optimization, environment variables, internal packages, monorepo structure/best practices, and boundaries. Use when user: configures tasks/workflows/pipelines, creates packages, sets up monorepo, shares code between apps, runs changed/affected packages, debugs cache, or has apps/packages directories.
turbo
Hotwire Turbo for Symfony UX -- SPA-like speed with zero JavaScript. Covers Drive (full-page navigation), Frames (partial page sections), and Streams (multi-target updates). Use when building ajax navigation, lazy-loaded page sections, inline editing, pagination without reload, modals loaded from the server, flash messages via streams, real-time updates via Mercure/SSE, or multi-section page updates. Code triggers: turbo-frame, turbo-stream, data-turbo-frame, data-turbo, data-turbo-action, turbo-stream-source, TurboStreamResponse, <twig:Turbo:Frame>, <twig:Turbo:Stream:Append>, <twig:Turbo:Stream:Replace>, turbo:before-fetch-request. Also trigger when the user asks "how to update part of the page without reload", "how to make navigation feel like SPA", "how to lazy-load a section", "how to do inline editing", "how to push real-time updates from server", "how to use Mercure with Turbo". Do NOT trigger for client-side JS behavior (use stimulus), server-rendered reactive components (use live-component), or reusable static UI (use twig-component).
turborepo
Guide for implementing Turborepo - a high-performance build system for JavaScript and TypeScript monorepos. Use when setting up monorepos, optimizing build performance, implementing task pipelines, configuring caching strategies, or orchestrating tasks across multiple packages.