Unnamed Skill
This skill should be used when the user asks about Hotwire, Turbo Drive, Turbo Frames, Turbo Streams, Stimulus controllers, frontend interactivity, server-rendered HTML updates, websocket updates, progressive enhancement, data attributes, importing JavaScript modules, or building interactive UIs without React/Vue. Also use when discussing the "HTML over the wire" approach or NO BUILD philosophy. Examples:
$ Instalar
git clone https://github.com/sjnims/rails-expert /tmp/rails-expert && cp -r /tmp/rails-expert/plugins/rails-expert/skills/hotwire-turbo-stimulus ~/.claude/skills/rails-expert// tip: Run this command in your terminal to install the skill
name: hotwire-turbo-stimulus description: This skill should be used when the user asks about Hotwire, Turbo Drive, Turbo Frames, Turbo Streams, Stimulus controllers, frontend interactivity, server-rendered HTML updates, websocket updates, progressive enhancement, data attributes, importing JavaScript modules, or building interactive UIs without React/Vue. Also use when discussing the "HTML over the wire" approach or NO BUILD philosophy. Examples:
Hotwire/Turbo/Stimulus: Modern Rails Frontend
Overview
Hotwire (HTML Over The Wire) is Rails' answer to frontend complexity. Instead of shipping JSON to a heavy JavaScript framework, Hotwire delivers HTML directly from the server.
Hotwire consists of:
- Turbo Drive: Fast navigation without full page reloads
- Turbo Frames: Update specific page sections
- Turbo Streams: Real-time HTML updates via WebSockets or HTTP
- Stimulus: Lightweight JavaScript controllers for sprinkles of interactivity
Together, they provide rich, reactive UIs with minimal JavaScript and no build step.
Philosophy
Hotwire reflects Rails 8's core principles:
- NO BUILD: No webpack, no complex toolchains
- Server-rendered: HTML comes from Rails, not JavaScript
- Progressive enhancement: Start with HTML, add JavaScript as needed
- Minimal JavaScript: Write only what browsers can't do
- Integrated: Works seamlessly with Rails conventions
Most applications need less JavaScript than you think. Hotwire proves it.
Turbo Drive
What It Does
Turbo Drive intercepts link clicks and form submissions, replacing full page loads with AJAX requests that update the page content.
Without Turbo Drive:
Click link → Browser requests page → Full page reload → JavaScript re-initializes
With Turbo Drive:
Click link → AJAX request → Replace <body> → Fast transition
Benefits:
- Instant navigation
- Preserved scroll position
- CSS/JS stay loaded
- Smooth transitions
How It Works
Automatically enabled when you include Turbo:
// app/javascript/application.js
import "@hotwired/turbo-rails"
Now all links and forms use Turbo Drive automatically:
<%= link_to "Products", products_path %>
<!-- Navigates via Turbo Drive -->
<%= form_with model: @product do |f| %>
<!-- Submits via Turbo Drive -->
<% end %>
Disabling Turbo Drive
For specific links/forms:
<%= link_to "External", "https://example.com", data: { turbo: false } %>
<%= form_with model: @product, data: { turbo: false } do |f| %>
<!-- Regular form submission -->
<% end %>
Turbo Drive Progress Bar
Built-in progress indicator for navigation:
/* Customize progress bar */
.turbo-progress-bar {
height: 5px;
background-color: #0076ff;
}
Turbo Frames
What They Do
Turbo Frames let you update specific page sections without affecting the rest of the page.
Traditional approach:
Update product → Full page reload → Entire page re-renders
With Turbo Frames:
Update product → Only product frame updates → Rest of page untouched
Basic Usage
<!-- app/views/products/index.html.erb -->
<h1>Products</h1>
<%= turbo_frame_tag "new_product" do %>
<%= link_to "New Product", new_product_path %>
<% end %>
<div id="products">
<%= render @products %>
</div>
<!-- app/views/products/new.html.erb -->
<%= turbo_frame_tag "new_product" do %>
<h2>New Product</h2>
<%= form_with model: @product do |f| %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>
<% end %>
When clicking "New Product", only the new_product frame updates—the rest of the page stays.
dom_id Helper
Rails provides dom_id for consistent frame IDs:
<%= turbo_frame_tag dom_id(@product) do %>
<%= render @product %>
<% end %>
<!-- Generates: <turbo-frame id="product_123">...</turbo-frame> -->
Frame Navigation
<!-- Clicking links inside frame navigates the frame -->
<%= turbo_frame_tag "products" do %>
<% @products.each do |product| %>
<%= link_to product.name, product_path(product) %>
<!-- Navigates the frame, not the page -->
<% end %>
<% end %>
Breaking Out of Frames
Navigate the full page from within a frame:
<%= link_to "View All", products_path, data: { turbo_frame: "_top" } %>
<!-- data-turbo-frame="_top" navigates the whole page -->
Lazy Loading Frames
Load content on demand:
<%= turbo_frame_tag "lazy_content", src: lazy_products_path do %>
Loading...
<% end %>
<!-- Automatically loads when frame appears in viewport -->
See references/turbo-frames.md for advanced patterns.
Turbo Streams
What They Do
Turbo Streams deliver targeted HTML updates after form submissions or via WebSockets.
Seven Stream Actions:
- append: Add to bottom of target
- prepend: Add to top of target
- replace: Replace entire target
- update: Replace target's content (keeps the target element)
- remove: Delete target
- before: Insert before target
- after: Insert after target
After Form Submission
# app/controllers/products_controller.rb
def create
@product = Product.new(product_params)
respond_to do |format|
if @product.save
format.turbo_stream { render turbo_stream: turbo_stream.prepend("products", @product) }
format.html { redirect_to @product }
else
format.turbo_stream { render turbo_stream: turbo_stream.replace(dom_id(@product), partial: "form", locals: { product: @product }) }
format.html { render :new, status: :unprocessable_entity }
end
end
end
<!-- app/views/products/index.html.erb -->
<div id="products">
<%= render @products %>
</div>
<%= turbo_frame_tag "new_product" do %>
<%= render "form", product: @product %>
<% end %>
When form submits, new product prepends to #products list without page reload.
Broadcast Updates (Real-Time)
Stream changes to all connected users:
# app/models/product.rb
class Product < ApplicationRecord
after_create_commit -> { broadcast_prepend_to "products", target: "products" }
after_update_commit -> { broadcast_replace_to "products" }
after_destroy_commit -> { broadcast_remove_to "products" }
end
<!-- app/views/products/index.html.erb -->
<%= turbo_stream_from "products" %>
<div id="products">
<%= render @products %>
</div>
Now when any user creates/updates/deletes a product, all connected users see the change instantly.
Multiple Stream Actions
# app/views/products/create.turbo_stream.erb
<%= turbo_stream.prepend "products", @product %>
<%= turbo_stream.update "counter", Product.count %>
<%= turbo_stream.replace "flash", partial: "shared/flash" %>
See references/turbo-streams.md for broadcasting patterns.
Stimulus
What It Does
Stimulus is a modest JavaScript framework for adding behavior to HTML.
Philosophy:
- HTML is the source of truth
- Controllers connect to HTML via data attributes
- Small, focused controllers
- Progressive enhancement
Basic Structure
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu"]
toggle() {
this.menuTarget.classList.toggle("hidden")
}
}
<!-- app/views/shared/_header.html.erb -->
<div data-controller="dropdown">
<button data-action="click->dropdown#toggle">Menu</button>
<nav data-dropdown-target="menu" class="hidden">
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
</div>
Data Attributes
Stimulus uses three data attributes:
- data-controller: Connects element to Stimulus controller
- data-{controller}-target: Marks element as a target
- data-action: Connects event to controller method
Targets
Reference elements in controllers:
export default class extends Controller {
static targets = ["input", "output", "button"]
connect() {
console.log(this.inputTarget) // First matching element
console.log(this.inputTargets) // All matching elements
console.log(this.hasInputTarget) // Boolean check
}
}
<div data-controller="example">
<input data-example-target="input">
<input data-example-target="input">
<div data-example-target="output"></div>
<button data-example-target="button">Click</button>
</div>
Actions
Connect events to methods:
<!-- Default event (click for buttons/links, input for form fields) -->
<button data-action="dropdown#toggle">Toggle</button>
<!-- Explicit event -->
<input data-action="keyup->search#query">
<!-- Multiple actions -->
<form data-action="submit->form#submit ajax:success->form#success">
<!-- Action options -->
<button data-action="click->menu#open:prevent">
<!-- :prevent calls preventDefault() -->
</button>
Values
Pass data to controllers:
export default class extends Controller {
static values = {
url: String,
count: Number,
active: Boolean
}
connect() {
console.log(this.urlValue)
console.log(this.countValue)
console.log(this.activeValue)
}
urlValueChanged(value, previousValue) {
// Called when value changes
}
}
<div data-controller="example"
data-example-url-value="<%= products_path %>"
data-example-count-value="5"
data-example-active-value="true">
</div>
See references/stimulus-controllers.md for controller patterns.
Integration Patterns
Turbo Frames + Stimulus
<div data-controller="auto-refresh" data-auto-refresh-interval-value="5000">
<%= turbo_frame_tag "stats", src: stats_path do %>
Loading...
<% end %>
</div>
// auto_refresh_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { interval: Number }
static targets = ["frame"]
connect() {
this.startRefreshing()
}
disconnect() {
this.stopRefreshing()
}
startRefreshing() {
this.refreshTimer = setInterval(() => {
this.element.querySelector('turbo-frame').reload()
}, this.intervalValue)
}
stopRefreshing() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
}
}
}
Inline Editing
<%= turbo_frame_tag dom_id(product) do %>
<div data-controller="inline-edit">
<span data-inline-edit-target="display">
<%= product.name %>
</span>
<button data-action="inline-edit#edit">Edit</button>
</div>
<% end %>
Common Patterns
Modal with Turbo Frame
<%= turbo_frame_tag "modal" %>
<%= link_to "New Product", new_product_path, data: { turbo_frame: "modal" } %>
Form renders inside modal frame.
Infinite Scroll
<div data-controller="infinite-scroll" data-action="scroll->infinite-scroll#loadMore">
<%= turbo_frame_tag "products", src: products_path(page: 1) %>
</div>
Live Search
<%= form_with url: search_products_path, method: :get, data: { turbo_frame: "results", turbo_action: "advance" } do |f| %>
<%= f.search_field :q, data: { action: "input->debounce#search" } %>
<% end %>
<%= turbo_frame_tag "results" do %>
<!-- Search results render here -->
<% end %>
Further Reading
For deeper exploration:
references/turbo-frames.md: Complete Turbo Frames guide with patternsreferences/turbo-streams.md: Broadcasting and real-time updatesreferences/stimulus-controllers.md: Building Stimulus controllers
For code examples (in examples/):
autosave_controller.js: Auto-save form datacharacter_counter_controller.js: Live character countingclipboard_controller.js: Copy to clipboard functionalityconfirm_controller.js: Confirmation dialogsdropdown_controller.js: Interactive dropdown menusform_controller.js: Form handling and validationinfinite_scroll_controller.js: Infinite scroll paginationmodal_controller.js: Modal dialogs with Stimulusnested_form_controller.js: Dynamic nested form fieldsremote_form_controller.js: AJAX form submissionssearch_controller.js: Real-time search filteringslideshow_controller.js: Image carousel/slideshowtabs_controller.js: Tab navigationtoggle_controller.js: Toggle visibility patterns
Summary
Hotwire provides:
- Turbo Drive: Fast navigation without full reloads
- Turbo Frames: Scoped page updates
- Turbo Streams: Real-time HTML over WebSockets
- Stimulus: Lightweight JavaScript controllers
- NO BUILD: No webpack, no complex tooling
- Server-rendered: HTML from Rails, not JSON APIs
Master Hotwire and build rich, interactive applications with minimal JavaScript.
Repository
