Building Modern UIs with Hotwire and Turbo in Rails

Building Modern UIs with Hotwire and Turbo in Rails

For years, "modern web app" meant React on the frontend and something else on the backend. Rails teams were pushed toward API-only mode and a JavaScript rewrite for every interactive screen. Hotwire flipped that script: keep Rails rendering HTML, add just enough JavaScript to make it feel fast.

What Hotwire Actually Is

Hotwire is two libraries working together:

  • Turbo — Handles navigation, partial updates, and real-time streams
  • Stimulus — Adds small JavaScript controllers for DOM behavior

The goal isn't zero JavaScript. It's the right amount of JavaScript, colocated with the HTML Rails already generates.

Turbo Drive: SPA Speed, Server Rendering

Turbo Drive intercepts link clicks and form submissions, fetches the next page in the background, and swaps the <body> without a full reload. Users get instant-feeling navigation; you keep server-side templates.

Enable it in a Rails 7+ app with:

# app/javascript/application.js
import "@hotwired/turbo-rails"

That's it for basic Turbo Drive. Your existing ERB or ViewComponent templates work unchanged.

Turbo Frames: Update Part of a Page

Frames are perfect for inline editing, lazy-loaded sections, and modal content. Wrap a region in a frame, and only that region updates on navigation:

<%= turbo_frame_tag "project_#{project.id}" do %>
  <h2><%= project.name %></h2>
  <%= link_to "Edit", edit_project_path(project) %>
<% end %>

The edit form loads inside the frame. Submit redirects back to the show view—and only the frame refreshes. No custom fetch logic required.

Common use cases:

  • Edit-in-place on index pages
  • Tabbed settings panels
  • Infinite scroll lists with loading: :lazy

Turbo Streams: Real-Time Updates

Streams push HTML fragments to the browser over Action Cable. After creating a record, broadcast a stream to append a row to a table:

# app/models/comment.rb
after_create_commit -> { broadcast_append_to project, target: "comments" }
<div id="comments">
  <%= render @project.comments %>
</div>

Every connected client sees the new comment instantly. Chat widgets, notification feeds, and collaborative UIs become straightforward.

Stimulus: Behavior, Not Framework

Stimulus controllers attach to HTML via data-controller attributes. Use them for toggles, autocomplete, clipboard copy, and form enhancements—without a component tree:

// 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")
  }
}
<div data-controller="dropdown">
  <button data-action="click->dropdown#toggle">Menu</button>
  <ul data-dropdown-target="menu" class="hidden">...</ul>
</div>

Keep controllers small. If you're building a 500-line Stimulus controller, consider a Turbo Frame or a dedicated view instead.

When to Reach for React Instead

Hotwire isn't always enough:

  • Rich client-side state — Complex drag-and-drop builders, canvas editors
  • Offline-first mobile — PWAs with heavy local sync
  • Third-party widget embedding — React often wins for npm ecosystem depth

The pragmatic approach: Hotwire for 80% of screens, React or Vue islands for the 20% that need them. Rails 7+ supports import maps and jsbundling for either path.

Performance Tips

  • Cache fragment partials inside Turbo Frames
  • Use turbo_prefetch on high-traffic links
  • Avoid huge DOM swaps—keep frame payloads small
  • Lazy-load frames below the fold

Testing Stays Simple

Because most logic lives in Ruby, system tests with Capybara cover real user flows. You're not mocking GraphQL layers or maintaining separate frontend test suites for every button.

Bottom Line

Hotwire lets Rails teams ship interactive products without splitting into two codebases. Turbo handles navigation and updates; Stimulus handles behavior. The result is faster development and fewer moving parts.

Want help modernizing a Rails app or starting fresh with Hotwire? Talk to us about your UI goals.