Hotwire, Turbo & Action Cable
Reviewed & published by Brayan K
Hotwire is the Rails 7 default front-end approach: build fast, reactive interfaces by sending HTML over the wire instead of writing a heavy JavaScript single-page app.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll understand Turbo Drive, Frames, and Streams, push live updates with Action Cable, and add small interactions with Stimulus.
What You'll Learn in This Lesson
- Hotwire: HTML over the wire
- Turbo Drive for SPA-like navigation
- Turbo Frames for partial page updates
- Turbo Streams: append, prepend, replace, remove
- Real-time broadcasts with Action Cable
- Stimulus controllers for modest JavaScript
1️⃣ Turbo Drive and Turbo Frames
Turbo Drive intercepts links and forms so navigation feels like an SPA with no full reloads. Turbo Frames scope updates to one region — a link inside a frame replaces only that frame.
# Hotwire = HTML Over The Wire. The server sends HTML, not JSON.
# Rails 7 includes Turbo and Stimulus by default.
# Turbo DRIVE: just include Turbo and ordinary links/forms become
# SPA-like — no full page reload. No code needed; it is on by default.
# Turbo FRAMES: wrap a region so only it updates.
# app/views/posts/show.html.erb
# <%= turbo_frame_tag "comments" do %>
# <%= render @post.comments %>
# <%= link_to "Add comment", new_comment_path %> # loads INTO the frame
# <% end %>
# A link or form inside a frame replaces only that frame's content,
# leaving the rest of the page untouched.2️⃣ Turbo Streams, Action Cable, and Stimulus
Turbo Streams send HTML fragments plus an action (append, replace, remove) to change the DOM, and can be broadcast live over a WebSocket with Action Cable.
# Turbo STREAMS push HTML fragments + an action to update the DOM.
# Actions: append, prepend, replace, update, remove, before, after.
# A controller can respond with a stream:
# app/views/comments/create.turbo_stream.erb
# <%= turbo_stream.append "comments" do %>
# <%= render @comment %>
# <% end %>
# BROADCAST in real time with Action Cable (WebSocket):
class Comment < ApplicationRecord
belongs_to :post
# Push new comments to everyone viewing the post, live:
after_create_commit -> { broadcast_append_to post, target: "comments" }
end
# In the view, subscribe to the stream:
# <%= turbo_stream_from @post %>
# <div id="comments"> ... </div>Stimulus attaches small bits of JavaScript to existing HTML using data attributes.
# STIMULUS adds modest JavaScript to existing HTML.
# app/javascript/controllers/hello_controller.js
#
# import { Controller } from "@hotwired/stimulus"
# export default class extends Controller {
# static targets = ["name", "output"]
# greet() {
# this.outputTarget.textContent = "Hello, " + this.nameTarget.value
# }
# }
# Wire it up with plain data attributes in the HTML:
# <div data-controller="hello">
# <input data-hello-target="name" type="text">
# <button data-action="click->hello#greet">Greet</button>
# <span data-hello-target="output"></span>
# </div>
# No build-your-own framework: Stimulus connects controllers to markup.Your turn. Wrap each todo in a Turbo Frame so editing swaps only that item.
Mini-Challenge: A Live List
Sketch a real-time messages feature that broadcasts new records to every viewer using Turbo Streams over Action Cable.
# 🎯 MINI-CHALLENGE: a live-updating list.
# Sketch (no need to run) a tiny "messages" feature:
#
# 1. A Message model that belongs_to :room.
# 2. broadcast_append_to the room after_create_commit, target "messages".
# 3. In the view: turbo_stream_from @room and a <div id="messages">.
# 4. Result: any browser viewing the room sees new messages appear live.
#
# ✅ Goal: real-time updates with NO custom JavaScript, just Hotwire.
# your code hereCommon Errors (and the fix)
- Mismatched frame ids — A link only swaps a frame when the response renders the same turbo_frame_tag id.
- Stream targets a missing id — The DOM must contain the target element (e.g. id="comments") for the action to apply.
- Forgetting turbo_stream_from — Without subscribing in the view, broadcasts never reach the browser.
- Reaching for an SPA framework — For many features Turbo and Stimulus replace a lot of custom JavaScript.
- Heavy logic in Stimulus — Keep controllers modest; render data with Rails and let Stimulus handle interactions.
Pro Tips
- 💡 Start with Frames: they solve partial updates with zero JavaScript.
- 💡 Broadcast from models: after_create_commit + broadcast_append_to gives live updates cheaply.
- 💡 Reuse partials: the same partial renders a normal page and a Turbo Stream fragment.
📋 Quick Reference — Hotwire
| Concept | Rails / HTML |
|---|---|
| SPA-like nav | Turbo Drive (default) |
| Partial update | turbo_frame_tag "id" |
| DOM action | turbo_stream.append |
| Subscribe | turbo_stream_from @room |
| Broadcast | broadcast_append_to |
| JS behavior | data-controller="hello" |
🎉 Lesson Complete!
- ✅ Hotwire sends HTML over the wire, not JSON
- ✅ Turbo Drive makes navigation feel like an SPA
- ✅ Turbo Frames scope updates to one region
- ✅ Turbo Streams change the DOM, even live via Action Cable
- ✅ Stimulus adds modest JavaScript to existing HTML
Practice quiz
What is Hotwire?
- an approach to building reactive apps by sending HTML, not JSON, over the wire
- a CSS framework
- a Ruby testing tool
- a database engine
Answer: an approach to building reactive apps by sending HTML, not JSON, over the wire. Hotwire (HTML Over The Wire) builds modern UIs by sending HTML from the server instead of JSON.
What does Turbo Drive do?
- stores sessions
- runs background jobs
- makes full-page navigation feel like a single-page app by intercepting links and forms
- compiles Ruby
Answer: makes full-page navigation feel like a single-page app by intercepting links and forms. Turbo Drive intercepts clicks and form submits, swapping the page body for SPA-like navigation without full reloads.
What is a Turbo Frame used for?
- writing migrations
- updating just a portion of the page independently
- encrypting cookies
- defining database tables
Answer: updating just a portion of the page independently. A turbo_frame_tag scopes updates to one region so only that part of the page changes.
What do Turbo Streams deliver?
- raw JSON arrays
- SQL queries
- CSS rules
- fragments of HTML with actions like append, prepend, replace, or remove
Answer: fragments of HTML with actions like append, prepend, replace, or remove. Turbo Streams send HTML fragments plus an action telling the browser how to update the DOM.
Which Turbo Stream action adds content to the end of a list?
- append
- update
- remove
- replace
Answer: append. The append action inserts the fragment at the end of the target element.
Over what transport can Turbo Streams be pushed from the server in real time?
- a cron job
- a WebSocket connection via Action Cable
- FTP
- plain email
Answer: a WebSocket connection via Action Cable. Turbo Streams can be broadcast over a WebSocket using Action Cable for live updates.
What is Stimulus?
- a Ruby gem builder
- a database adapter
- a CSS preprocessor
- a modest JavaScript framework that attaches behavior to existing HTML
Answer: a modest JavaScript framework that attaches behavior to existing HTML. Stimulus is a lightweight JS framework that wires controllers to HTML via data attributes.
How does a Stimulus controller connect to markup?
- through SQL triggers
- through inline onclick handlers
- via data-controller and data-action attributes
- by editing routes.rb
Answer: via data-controller and data-action attributes. Stimulus uses data-controller, data-action, and data-target attributes to bind behavior to HTML.
In Rails 7, Hotwire is best described as:
- a database tool
- the default front-end approach
- a deployment service
- a deprecated plugin
Answer: the default front-end approach. Hotwire (Turbo plus Stimulus) ships as the default front-end stack in Rails 7.
What is Action Cable?
- a Rails framework for real-time features over WebSockets
- a CSS grid system
- a job scheduler
- a templating language
Answer: a Rails framework for real-time features over WebSockets. Action Cable integrates WebSockets into Rails, enabling real-time broadcasts like Turbo Stream updates.
Continue this course
- Previous: Background Jobs with Active Job & Sidekiq
- Next: Building & Publishing a Gem — Create a gemspec, structure lib/, and publish to RubyGems.org
- Quick reference: Ruby cheat sheet
Frequently asked questions
What is Hotwire and how is it different from a JavaScript SPA?
Hotwire stands for HTML Over The Wire. Instead of a heavy JavaScript front-end that fetches JSON and renders the UI in the browser, Hotwire keeps rendering on the server and sends HTML fragments to the browser, which swaps them into the page. It is made of Turbo (Drive, Frames, and Streams) plus Stimulus for the small amount of JavaScript you still need. Compared with a classic SPA, you write far less JavaScript, keep your logic and templates in Rails, and still get fast, app-like interactions. It is the default front-end approach in Rails 7, designed to deliver modern UX with server-rendered simplicity.
What is the difference between Turbo Frames and Turbo Streams?
Turbo Frames scope a part of the page so that links and forms inside the frame update only that region, leaving the rest untouched — great for things like inline editing, pagination, or tabbed content, and they are driven by the user clicking or submitting. Turbo Streams go further: they send one or more HTML fragments each paired with an action (append, prepend, replace, update, remove, before, or after) telling the browser exactly how to change the DOM, and they can target multiple elements at once. Crucially, Streams can be pushed from the server in real time over a WebSocket, so Frames are mostly request-driven while Streams enable both response-driven and live, broadcast updates.
How do Turbo Streams deliver real-time updates?
Turbo Streams can be broadcast from the server over a WebSocket using Action Cable, Rails' real-time framework. In a model you call something like broadcast_append_to after a record is created, naming a stream and a target element; in the view you subscribe with turbo_stream_from. When the event fires, Rails renders a small HTML fragment and pushes it to every subscribed browser, which applies the stream action to update the DOM instantly. The result is live features — chat messages, notifications, collaborative lists — with essentially no custom JavaScript, because the server is sending ready-to-insert HTML rather than JSON that the client must turn into markup.
Where does Stimulus fit in?
Stimulus is the modest JavaScript half of Hotwire. Rather than rendering your UI, it sprinkles behavior onto HTML the server already produced. You write small controller classes and connect them to markup using data attributes: data-controller names the controller, data-action binds events like click to methods, and data-target marks elements the controller reads or updates. This keeps JavaScript organized and reusable without taking over rendering. Stimulus handles the interactions Turbo does not — toggling menus, copying to clipboard, debouncing input — while Turbo handles navigation and DOM updates, together giving you a reactive app with very little custom front-end code.