turbo-rails: Turbo Drive, Frames and Streams inside a Rails app
Use Turbo in your Ruby on Rails app
At a glance
- What is it?
- The turbo-rails gem wires the Turbo JavaScript library into Rails conventions: link and form interception, turbo_frame_tag, turbo_stream_from and model-callback broadcasts over Action Cable. It suits Rails teams that want SPA-style navigation without writing a JavaScript front end.
- Who is it for?
- Adopt turbo-rails if your app already renders server-side HTML and you want faster navigation plus partial updates without building a JSON API. Skip it if you rely on custom layout resolution and are not ready to return "turbo_rails/frame" for frame requests, or if you need offline-first behaviour, which the README does not cover.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 91 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What turbo-rails solves for a Rails codebase
Rails already renders HTML on the server. The cost of that choice shows up on every click: the browser throws away the page and rebuilds it, and any interaction that should touch only one region of the screen turns into hand-written JavaScript. turbo-rails exists to remove that trade-off for Rails applications specifically.
The gem is the Rails integration layer for Turbo, which the README describes as a language-agnostic framework written in JavaScript. Turbo itself handles link interception, frame replacement and stream updates in the browser. The gem adds the Rails side: helpers such as turbo_frame_tag and turbo_stream_from, a layout registered for frame requests, model callbacks that broadcast over Action Cable through Active Job, and a packaged JavaScript entry point for the asset pipeline.
The intended audience is a Rails team that wants the navigation feel of a single-page application while keeping server-rendered views. The README frames the payoff as reducing the amount of custom JavaScript many web applications need, and points to Stimulus for whatever dynamic behaviour remains. If your front end is already a separate JavaScript application consuming a JSON API, the gem's central assumptions do not apply to you.
How Turbo Drive, Frames and Streams move data
Turbo Drive intercepts clicks on same-domain <a href> links. The README states that Turbo prevents the browser from following the link, changes the URL with the History API, requests the page with fetch, then renders the response by replacing the <body> element outright and merging the contents of <head>. The window, document and <html> element persist across renderings. Form submissions and responses go through the same path, which is why the README notes that data-remote=true is no longer needed.
Turbo Frames narrow that replacement to a region. The gem provides turbo_frame_tag to declare the region, and the README's example shows a show view and an edit view both wrapped in turbo_frame_tag @todo; clicking the edit link replaces only that frame with the matching frame from the edit page. Because frames have their own cache timeline, a personalized fragment such as a toolbar can be lazy-loaded by an otherwise publicly cached page.
Turbo Streams carry partial updates asynchronously over a web socket. The README describes updating the page with CRUD-like container tags and HTML fragments, without a separate API or JSON. In Rails terms, the gem uses Active Job for asynchronous partial rendering and Action Cable to deliver updates to subscribers, and turbo_stream_from dom_id(@todo) subscribes the page to that stream. The same HTML you render for the first page load is what gets wrapped in an update tag.
Installing turbo-rails and rendering a first frame
The gem is distributed as turbo-rails and the JavaScript package as @hotwired/turbo-rails. The package.json in the repository lists @hotwired/turbo as a dependency and @rails/actioncable at >=7.0, so Action Cable has to be available for stream broadcasts. The README does not spell out a full install sequence, so treat the code below as the shape of the integration rather than a copy of a documented procedure.
The README shows the JavaScript import used to load Turbo, and that import is where Drive can be turned off globally:
import "@hotwired/turbo-rails"
Turbo.session.drive = falseWith Drive disabled by default, data-turbo="true" re-enables it per element. The reverse also works: annotate an element or any ancestor with data-turbo="false" to opt that subtree out.
For a first real use, wrap a record in a frame in the show view and provide a matching frame in the edit view, as the README example does:
<%# app/views/todos/show.html.erb %>
<%= turbo_frame_tag @todo do %>
<p><%= @todo.description %></p>
<%= link_to 'Edit this todo', edit_todo_path(@todo) %>
<% end %>Clicking "Edit this todo" should replace only the frame with the frame returned by edit_todo_path, not the whole page. If the entire page is replaced, the usual cause is a layout problem rather than a frame problem, which the next section covers.
The custom layout trap that breaks frame rendering
This is the sharpest constraint in the README, and it is easy to hit in an existing application. To render frame requests without the application layout, Turbo registers a custom layout method. If your application uses custom layout resolution, the README says you have to return "turbo_rails/frame" for turbo frame requests, or false for TurboRails before 1.4.0.
A layout declared as a static string is the failure mode:
layout "some_static_layout"The README states that you have to change this to a layout method so it can conditionally return the frame layout:
layout :custom_layout
def custom_layout
return "turbo_rails/frame" if turbo_frame_request?
"some_static_layout"
endIf your layout logic is more involved, the same rule applies: return "turbo_rails/frame" when turbo_frame_request? is true, then fall through to your own resolution. The symptom of getting this wrong is a frame request answered with a full page, which the frame then renders inside the frame element. That is a design trade-off baked into the integration, not a bug you can configure around.
Where turbo-rails is the wrong tool
The gem assumes the server renders HTML that the browser can swap in. Applications built as a JSON API with a separate client do not fit that model, and adding turbo-rails to one buys nothing. If you need offline-first behaviour, background sync, or a client that owns its own routing and state, nothing in the README addresses those cases.
There is also a testing cost. The README includes a section titled "Testing Turbo Stream Broadcasts" and states that receiving server-generated Turbo Broadcasts requires something more, but the excerpt ends there. If your test suite needs to assert on broadcast delivery, plan to read the full documentation rather than assuming the gem ships a complete testing story.
Finally, the asynchronous path depends on Active Job and Action Cable. The README states plainly that Turbo uses Active Jobs for asynchronous partial rendering and Action Cable to deliver updates to subscribers. An application without a working Action Cable setup cannot use the broadcast half of the gem, and the npm dependency on @rails/actioncable at >=7.0 makes that requirement explicit.
turbo-rails compared with writing your own fetch layer
The obvious alternative is doing this by hand: intercept link clicks, call fetch, swap the DOM, and manage history yourself, or bring in a client-side framework that owns rendering. The difference is where the page structure lives. A hand-written layer has to decide what to replace, how to merge head contents, and how to keep the window and document objects alive across navigations. Turbo Drive makes those decisions for you: the README says the body is replaced outright and head contents are merged, with window, document and html persisting.
A client-side framework takes the opposite route. It renders from data in the browser, so the server's job becomes producing JSON. Turbo Streams deliberately avoid that: the README says you take the HTML you are already making, wrap it in an update tag, and the page changes, with no separate API and no JSON. The trade-off is that your update semantics are constrained to the container tags Turbo understands, and your cache keys are tied to the fragments you broadcast.
Turbo is also a continuation of Turbolinks, and the README notes that the earlier approach lives on as Turbo Drive. Teams migrating from Turbolinks are moving along a supported path rather than adopting an unrelated tool, though the migration itself is not described in the README.
Maintenance, upgrade cost and the MIT licence
The repository is not archived, and the last push was on 2026-07-01. Releases in 2026 include v2.0.21 on 2026-01-16, v2.0.22 on 2026-01-28 and v2.0.23 on 2026-01-29. The gem and the npm package version in lockstep in places: package.json declares version 8.0.23 and depends on @hotwired/turbo at ^8.0.23, while the gem releases are numbered in the 2.0.x series. Keep that split in mind when you pin versions, because the Ruby gem and the JavaScript package do not share a version number.
The repository ships an UPGRADING.md file at the top level, which is where the project keeps its upgrade instructions. The README itself does not document rollback, so plan upgrades around that file rather than expecting the README to describe reverting.
Both the gem and the npm package are MIT licensed, and the repository carries an MIT-LICENSE file. MIT is permissive, but this is a description of the licence identifier, not legal advice; check how the licence and the bundled @hotwired/turbo dependency interact with your own distribution model before shipping.
Editorial conclusion
Adopt turbo-rails if your app already renders server-side HTML and you want faster navigation plus partial updates without building a JSON API. Skip it if you rely on custom layout resolution and are not ready to return "turbo_rails/frame" for frame requests, or if you need offline-first behaviour, which the README does not cover. Before adopting, verify that your layout is a method rather than a static string, that your Rails and Action Cable versions satisfy the gem and npm dependency ranges, and read UPGRADING.md for the changes between Turbo versions.
Frequently asked questions
What is Turbo Stream in Rails?
Turbo Streams deliver partial page updates asynchronously over a web socket connection, using CRUD-like container tags and HTML fragments rather than JSON. In this gem, turbo_stream_from subscribes a page to a stream, and updates are rendered asynchronously through Active Job and delivered with Action Cable.
What is Turbo in JavaScript?
Turbo is a language-agnostic framework written in JavaScript that accelerates links and form submissions, splits a page into independent frames, and applies partial page updates from HTML. turbo-rails is the Rails integration built on top of it.
What is turbo-rails?
turbo-rails is the gem that integrates Turbo with Rails. It provides helpers such as turbo_frame_tag and turbo_stream_from, a layout registered for frame requests, model-callback broadcasts over Action Cable, and a packaged JavaScript entry point.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hotwired-turbo-rails)