hotwire

star 0

Hotwire, Turbo Drive, Turbo Frames, Turbo Streams, Stimulus controllers. Use when building interactive UI, dynamic updates, or JavaScript behavior.

dinum-HubEE By dinum-HubEE schedule Updated 4/26/2026

name: hotwire description: Hotwire, Turbo Drive, Turbo Frames, Turbo Streams, Stimulus controllers. Use when building interactive UI, dynamic updates, or JavaScript behavior. globs: - "app/views//*.erb" - "app/views//.turbo_stream.erb" - "app/javascript/controllers/**/.js"

Hotwire Skill

Turbo Drive

Turbo Drive is enabled by default. It intercepts link clicks and form submissions, replacing page content via AJAX.

Disabling for specific links

<!-- Disable Turbo for this link -->
<%= link_to "External", "https://example.com", data: { turbo: false } %>

<!-- Force full page reload -->
<%= link_to "Reload", path, data: { turbo_action: "replace" } %>

Turbo Frames

Turbo Frames allow partial page updates.

Basic Frame

<!-- app/views/subscriptions/index.html.erb -->
<%= turbo_frame_tag "subscriptions" do %>
  <% @subscriptions.each do |subscription| %>
    <%= render subscription %>
  <% end %>
<% end %>

Lazy Loading

<!-- Load content lazily when frame becomes visible -->
<%= turbo_frame_tag "stats", src: stats_path, loading: :lazy do %>
  <p>Loading stats...</p>
<% end %>

Breaking out of frames

<!-- Link targets the whole page, not the frame -->
<%= link_to "View Details", subscription_path(@subscription),
    data: { turbo_frame: "_top" } %>

Frame Navigation

<!-- Form that updates a different frame -->
<%= form_with url: search_path, data: { turbo_frame: "results" } do |f| %>
  <%= f.search_field :q %>
  <%= f.submit "Search" %>
<% end %>

<%= turbo_frame_tag "results" do %>
  <!-- Search results appear here -->
<% end %>

Turbo Streams

Real-time DOM updates from the server.

Stream Actions

<!-- app/views/subscriptions/create.turbo_stream.erb -->

<!-- Append to a list -->
<%= turbo_stream.append "subscriptions" do %>
  <%= render @subscription %>
<% end %>

<!-- Prepend to a list -->
<%= turbo_stream.prepend "subscriptions" do %>
  <%= render @subscription %>
<% end %>

<!-- Replace an element -->
<%= turbo_stream.replace @subscription do %>
  <%= render @subscription %>
<% end %>

<!-- Update content (keeps the element) -->
<%= turbo_stream.update "flash" do %>
  <%= render "shared/flash" %>
<% end %>

<!-- Remove an element -->
<%= turbo_stream.remove @subscription %>

Controller Response

def create
  @subscription = Subscription.new(subscription_params)

  respond_to do |format|
    if @subscription.save
      format.turbo_stream
      format.html { redirect_to @subscription }
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end

Stimulus Controllers

Basic Controller

// app/javascript/controllers/toggle_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["content"]
  static values = { open: Boolean }

  toggle() {
    this.openValue = !this.openValue
  }

  openValueChanged() {
    this.contentTarget.classList.toggle("hidden", !this.openValue)
  }
}
<div data-controller="toggle" data-toggle-open-value="false">
  <button data-action="toggle#toggle">Toggle</button>
  <div data-toggle-target="content" class="hidden">
    Content here
  </div>
</div>

Form Controller

// app/javascript/controllers/form_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["submit"]

  connect() {
    this.validate()
  }

  validate() {
    const form = this.element
    const isValid = form.checkValidity()
    this.submitTarget.disabled = !isValid
  }
}
<%= form_with model: @subscription, data: { controller: "form" } do |f| %>
  <%= f.text_field :name, required: true, data: { action: "input->form#validate" } %>
  <%= f.submit "Save", data: { form_target: "submit" } %>
<% end %>

Debounce Search

// app/javascript/controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input"]
  static values = { url: String }

  search() {
    clearTimeout(this.timeout)
    this.timeout = setTimeout(() => {
      const query = this.inputTarget.value
      Turbo.visit(`${this.urlValue}?q=${encodeURIComponent(query)}`, {
        frame: "results"
      })
    }, 300)
  }
}

Common Patterns

Flash Messages with Turbo

<!-- app/views/layouts/application.html.erb -->
<%= turbo_frame_tag "flash" do %>
  <%= render "shared/flash" %>
<% end %>
<!-- Update flash in turbo_stream response -->
<%= turbo_stream.update "flash" do %>
  <div class="alert alert-success">Saved!</div>
<% end %>

Modal with Turbo Frame

<!-- Link opens modal -->
<%= link_to "Edit", edit_subscription_path(@subscription),
    data: { turbo_frame: "modal" } %>

<!-- Modal frame (empty by default) -->
<%= turbo_frame_tag "modal" %>

<!-- edit.html.erb targets the modal frame -->
<%= turbo_frame_tag "modal" do %>
  <div class="modal">
    <%= render "form" %>
  </div>
<% end %>
Install via CLI
npx skills add https://github.com/dinum-HubEE/hubee-claude-plugin --skill hotwire
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator