Hotwire: Turbo, Stimulus & Hotwire Native
Hotwire (HTML Over The Wire) is an approach, not a bundle: send server-rendered
HTML instead of JSON, and keep client-side JavaScript minimal. Three tools,
one escalation ladder — always start at the top:
- Turbo Drive — every app has it for free: links and forms become fetch
visits, no full page reloads. Zero code.
- Morphing page refreshes — smooth full-page updates that preserve
scroll/focus; broadcast a "refresh" signal for real-time with almost no
code.
- Turbo Frames — scope navigation to a page region (inline edit, tabs,
lazy panels).
- Turbo Streams — surgical CRUD mutations of specific elements, from form
responses or WebSocket broadcasts.
- Stimulus — the JavaScript sprinkle for what HTML-over-the-wire can't
express (clipboard, drag, keyboard, third-party widgets).
- Hotwire Native — wrap the finished web app in real iOS/Android shells;
upgrade individual screens/controls to native only where it pays.
Choose the lowest rung that solves the problem; most "we need Streams" cases
are a Frame, and most "we need a Stimulus controller" cases are a Turbo
attribute.
Version facts (verified 2026-08-31)
- Turbo 8.0.23 (Jan 2026) — npm
@hotwired/turbo, Rails gem
turbo-rails. Turbo 8 added morphing page refreshes.
- Stimulus 3.2.2 — npm
@hotwired/stimulus, Rails gem stimulus-rails.
- Hotwire Native: iOS 1.3.1 (2026-08-14), Android 1.3.1 (2026-07-27); web bridge npm
@hotwired/hotwire-native-bridge. Hotwire Native supersedes the old
"Turbo Native" + "Strada" pair (Strada lives on as bridge components).
- All are backend-agnostic; in Rails they ship by default via importmap.
Verify versions with a web search if the current date is well past early
Non-negotiable ground rules
- Server responses drive everything. After a failed form submit, respond
422 Unprocessable Content (:unprocessable_content on Rails 8.1 / Rack 3.1+; the older
:unprocessable_entity still means 422 but warns); after a successful mutation, redirect with
303 See Other. Turbo silently misbehaves without these statuses.
- IDs are the contract. Frames match on
id; stream actions target id
(or CSS with targets). Generate them consistently (dom_id(record) in
Rails).
- Progressive enhancement. Everything must work as plain HTML requests
first; Frames/Streams/Stimulus layer on top. A feature that only works with
JS enabled is a design smell in Hotwire.
- State lives in the DOM. Stimulus values/classes/targets read and write
the document; no client-side stores.
Reference files — read before working in an area
| Read |
When the task involves |
references/turbo.md |
Drive (visits, caching, prefetch, view transitions), morphing page refreshes, Frames (eager/lazy, targeting, breakout), Streams (the 8 actions, broadcasts, custom actions), events, turbo-rails helpers |
references/stimulus.md |
Controllers, lifecycle, actions (descriptors, options, key filters, parameters), targets, values, CSS classes, outlets, cross-controller communication, patterns and anti-patterns |
references/native.md |
Hotwire Native iOS/Android setup, navigation and the routing table, path configuration JSON, bridge components (web + Swift + Kotlin), native screens, web-side detection, turbo-rails native helpers |
references/production.md |
Anything real-time or interactive. How two shipped 37signals apps use this stack: choosing refreshes vs hand-written broadcasts, optimistic UI and stream de-duplication, catching up after a dropped WebSocket, morph hazards (open dialogs, edits in progress), Action Cable vs Streams, presence, drag-and-drop, Stimulus organisation at 35–69 controllers |
A typical feature crosses files: a live-updating list with a native app is
Streams (turbo.md) + a controller sprinkle (stimulus.md) + path config
(native.md). Before building anything that pushes updates to a live page,
read production.md §0 — it is the escalation test for which rung of the
ladder the feature actually needs.
Working in a Rails app
The rails-8 skill owns Rails-side integration (broadcast models, view
helpers in ERB, testing Turbo responses, importmap pins); this skill owns the
Hotwire frameworks themselves. Load both for Rails frontend work. In non-Rails
backends, everything here still applies — install via npm/importmap CDN, and
replace turbo-rails broadcast helpers with your framework's WebSocket/SSE
channel emitting <turbo-stream> HTML.
Definition of done for Hotwire work
State which rung of the ladder you used and why. Verify: the flow works with
a hard refresh (no-JS baseline), form errors re-render with 422, mutations
redirect with 303, Frames have matching ids on both ends, broadcasts render
from a model/job context without controller state, and Stimulus controllers
clean up in disconnect(). For Native work: the path configuration handles
the new routes and the screen behaves on both platforms or is explicitly
platform-scoped.
If the page depends on broadcasts to stay correct, one more item is
mandatory: a catch-up path. Streams are fire-and-forget — nothing replays
what was missed while the socket was down, so a broadcast-only page is
silently stale after every network blip. Name the high-water mark, the
?since= endpoint that returns the missed streams, and what triggers it
(cable reconnect, and regaining visibility after a long hide).
production.md §1.4 has the worked shape.
1---2name: hotwire3description: Deep reference for the Hotwire stack from the official handbooks — Turbo (Drive, Frames, Streams, morphing page refreshes), Stimulus (controllers, actions, targets, values, outlets), and Hotwire Native (wrap a web app into iOS and Android apps with bridge components and path configuration). Use this skill whenever the user works with Turbo or Stimulus in any backend (Rails, Laravel, Django, Phoenix, plain HTML), mentions turbo-rails, stimulus-rails, @hotwired packages, turbo_frame_tag, turbo_stream, broadcasts, data-controller/data-action/data-*-target attributes, morphing, "SPA-like without a SPA", partial page updates, live updates over WebSockets — or wants a mobile app from their web app: Hotwire Native, Turbo Native, Strada, bridge components, path configuration, WKWebView/ webview wrapper apps, or "turn my Rails app into an iOS/Android app". Also covers production Hotwire patterns extracted from shipped 37signals apps: real-time chat-scale broadcasting, optimistic UI, presence, typing indicators, unread4---56# Hotwire: Turbo, Stimulus & Hotwire Native78Hotwire (HTML Over The Wire) is an approach, not a bundle: send server-rendered9HTML instead of JSON, and keep client-side JavaScript minimal. Three tools,10one escalation ladder — always start at the top:11121. **Turbo Drive** — every app has it for free: links and forms become fetch13 visits, no full page reloads. Zero code.142. **Morphing page refreshes** — smooth full-page updates that preserve15 scroll/focus; broadcast a "refresh" signal for real-time with almost no16 code.173. **Turbo Frames** — scope navigation to a page region (inline edit, tabs,18 lazy panels).194. **Turbo Streams** — surgical CRUD mutations of specific elements, from form20 responses or WebSocket broadcasts.215. **Stimulus** — the JavaScript sprinkle for what HTML-over-the-wire can't22 express (clipboard, drag, keyboard, third-party widgets).236. **Hotwire Native** — wrap the finished web app in real iOS/Android shells;24 upgrade individual screens/controls to native only where it pays.2526Choose the *lowest* rung that solves the problem; most "we need Streams" cases27are a Frame, and most "we need a Stimulus controller" cases are a Turbo28attribute.2930## Version facts (verified 2026-08-31)3132- **Turbo 8.0.23** (Jan 2026) — npm `@hotwired/turbo`, Rails gem33 `turbo-rails`. Turbo 8 added morphing page refreshes.34- **Stimulus 3.2.2** — npm `@hotwired/stimulus`, Rails gem `stimulus-rails`.35- **Hotwire Native**: iOS **1.3.1** (2026-08-14), Android **1.3.1** (2026-07-27); web bridge npm36 `@hotwired/hotwire-native-bridge`. Hotwire Native supersedes the old37 "Turbo Native" + "Strada" pair (Strada lives on as *bridge components*).38- All are backend-agnostic; in Rails they ship by default via importmap.39 Verify versions with a web search if the current date is well past early40 2026.4142## Non-negotiable ground rules4344- **Server responses drive everything.** After a failed form submit, respond45 `422 Unprocessable Content` (`:unprocessable_content` on Rails 8.1 / Rack 3.1+; the older46 `:unprocessable_entity` still means 422 but warns); after a successful mutation, redirect with47 `303 See Other`. Turbo silently misbehaves without these statuses.48- **IDs are the contract.** Frames match on `id`; stream actions target `id`49 (or CSS with `targets`). Generate them consistently (`dom_id(record)` in50 Rails).51- **Progressive enhancement.** Everything must work as plain HTML requests52 first; Frames/Streams/Stimulus layer on top. A feature that only works with53 JS enabled is a design smell in Hotwire.54- **State lives in the DOM.** Stimulus values/classes/targets read and write55 the document; no client-side stores.5657## Reference files — read before working in an area5859| Read | When the task involves |60|---|---|61| `references/turbo.md` | Drive (visits, caching, prefetch, view transitions), morphing page refreshes, Frames (eager/lazy, targeting, breakout), Streams (the 8 actions, broadcasts, custom actions), events, `turbo-rails` helpers |62| `references/stimulus.md` | Controllers, lifecycle, actions (descriptors, options, key filters, parameters), targets, values, CSS classes, outlets, cross-controller communication, patterns and anti-patterns |63| `references/native.md` | Hotwire Native iOS/Android setup, navigation and the routing table, path configuration JSON, bridge components (web + Swift + Kotlin), native screens, web-side detection, turbo-rails native helpers |64| `references/production.md` | **Anything real-time or interactive.** How two shipped 37signals apps use this stack: choosing refreshes vs hand-written broadcasts, optimistic UI and stream de-duplication, catching up after a dropped WebSocket, morph hazards (open dialogs, edits in progress), Action Cable vs Streams, presence, drag-and-drop, Stimulus organisation at 35–69 controllers |6566A typical feature crosses files: a live-updating list with a native app is67Streams (`turbo.md`) + a controller sprinkle (`stimulus.md`) + path config68(`native.md`). Before building anything that pushes updates to a live page,69read `production.md` §0 — it is the escalation test for which rung of the70ladder the feature actually needs.7172## Working in a Rails app7374The **rails-8** skill owns Rails-side integration (broadcast models, view75helpers in ERB, testing Turbo responses, importmap pins); this skill owns the76Hotwire frameworks themselves. Load both for Rails frontend work. In non-Rails77backends, everything here still applies — install via npm/importmap CDN, and78replace `turbo-rails` broadcast helpers with your framework's WebSocket/SSE79channel emitting `<turbo-stream>` HTML.8081## Definition of done for Hotwire work8283State which rung of the ladder you used and why. Verify: the flow works with84a hard refresh (no-JS baseline), form errors re-render with 422, mutations85redirect with 303, Frames have matching ids on both ends, broadcasts render86from a model/job context without controller state, and Stimulus controllers87clean up in `disconnect()`. For Native work: the path configuration handles88the new routes and the screen behaves on both platforms or is explicitly89platform-scoped.9091**If the page depends on broadcasts to stay correct, one more item is92mandatory: a catch-up path.** Streams are fire-and-forget — nothing replays93what was missed while the socket was down, so a broadcast-only page is94silently stale after every network blip. Name the high-water mark, the95`?since=` endpoint that returns the missed streams, and what triggers it96(cable reconnect, and regaining visibility after a long hide).97`production.md` §1.4 has the worked shape.