Skip to content
hotwire-turbo-stimulus logo

Hotwire/Turbo/Stimulus: Modern Rails Frontend

hotwire-turbo-stimulus

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 ...

SKILL.md

Full skill instructions

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:

  1. data-controller: Connects element to Stimulus controller
  2. data-{controller}-target: Marks element as a target
  3. 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 patterns
  • references/​turbo-streams.md: Broadcasting and real-time updates
  • references/​stimulus-controllers.md: Building Stimulus controllers

For code examples (in examples/):

  • autosave_controller.js: Auto-save form data
  • character_counter_controller.js: Live character counting
  • clipboard_controller.js: Copy to clipboard functionality
  • confirm_controller.js: Confirmation dialogs
  • dropdown_controller.js: Interactive dropdown menus
  • form_controller.js: Form handling and validation
  • infinite_scroll_controller.js: Infinite scroll pagination
  • modal_controller.js: Modal dialogs with Stimulus
  • nested_form_controller.js: Dynamic nested form fields
  • remote_form_controller.js: AJAX form submissions
  • search_controller.js: Real-time search filtering
  • slideshow_controller.js: Image carousel/​slideshow
  • tabs_controller.js: Tab navigation
  • toggle_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.

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View