This skill should be used when the user asks about "Hotwire", "Turbo", "Turbo Drive", "Turbo Frames", "Turbo Streams", "Stimulus", "Stimulus controllers", "Stimulus values", "Stimulus targets", "Stimulus actions", "import maps", "live updates", "partial page updates", "SPA-like behavior", "real-time updates", "turbo_frame_tag", "turbo_stream", "broadcast", or needs guidance on building modern Rails 7+ frontends without heavy JavaScript frameworks.
/plugin marketplace add bastos/rails-plugin/plugin install bastos-ruby-on-rails@bastos/rails-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/stimulus-patterns.mdreferences/turbo-streams-advanced.mdComprehensive guide to building modern, reactive web applications with Hotwire (Turbo + Stimulus) in Rails 7+.
Hotwire enables "fast, modern, progressively enhanced web applications without using much JavaScript." It sends HTML over the wire instead of JSON, keeping business logic on the server.
Three components:
<body>Turbo Drive intercepts link clicks and form submissions, fetching via fetch and replacing <body> while merging <head>. The window, document, and <html> persist across navigations.
<%# Disable Turbo for external links or file downloads %>
<%= link_to "Download PDF", pdf_path, data: { turbo: false } %>
<%# Disable for forms that need full page reload %>
<%= form_with model: @export, data: { turbo: false } do |f| %>
.turbo-progress-bar {
background-color: #3b82f6;
height: 3px;
}
Frames wrap page segments in <turbo-frame> elements for scoped navigation. Only matching frames extract from server responses.
| Attribute | Purpose |
|---|---|
id | Required. Unique identifier to match frames between requests |
src | URL for eager-loading frame content on page load |
loading="lazy" | Delay loading until frame becomes visible |
target | Navigation scope: _top (full page), _self, or frame ID |
data-turbo-action | Promote to browser history (advance or replace) |
<%# app/views/articles/show.html.erb %>
<h1><%= @article.title %></h1>
<%= turbo_frame_tag @article do %>
<p><%= @article.body %></p>
<%= link_to "Edit", edit_article_path(@article) %>
<% end %>
<%# app/views/articles/edit.html.erb %>
<%= turbo_frame_tag @article do %>
<%= render "form", article: @article %>
<% end %>
<%# Load content when frame becomes visible %>
<%= turbo_frame_tag "comments",
src: article_comments_path(@article),
loading: :lazy do %>
<p>Loading comments...</p>
<% end %>
<%# Navigate entire page %>
<%= link_to "View Full", article_path(@article), data: { turbo_frame: "_top" } %>
<%# Target different frame %>
<%= link_to "Preview", preview_path(@article), data: { turbo_frame: "preview_panel" } %>
During navigation, frames receive [aria-busy="true"]. After completion, [complete] is set on the frame.
Streams deliver DOM changes as <turbo-stream> elements specifying action and target.
| Action | Description |
|---|---|
append | Add content to end of target |
prepend | Add content to start of target |
replace | Replace entire target element |
update | Replace target's inner content only |
remove | Delete target element |
before | Insert before target element |
after | Insert after target element |
morph | Intelligently morph changes (variant of replace/update) |
refresh | Refresh page or frame |
# app/controllers/comments_controller.rb
def create
@comment = @article.comments.build(comment_params)
if @comment.save
respond_to do |format|
format.turbo_stream # Renders create.turbo_stream.erb
format.html { redirect_to @article }
end
else
render :new, status: :unprocessable_entity
end
end
<%# app/views/comments/create.turbo_stream.erb %>
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comment_count", @article.comments.count %>
<%= turbo_stream.replace "new_comment", partial: "form", locals: { comment: Comment.new } %>
def destroy
@comment.destroy
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.remove(@comment),
turbo_stream.update("comment_count", @article.comments.count)
]
end
format.html { redirect_to @article }
end
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :article
after_create_commit -> {
broadcast_append_to article, target: "comments"
}
after_destroy_commit -> {
broadcast_remove_to article
}
end
<%# Subscribe to broadcasts %>
<%= turbo_stream_from @article %>
<div id="comments">
<%= render @article.comments %>
</div>
<%# Persistent connection for real-time updates %>
<turbo-stream-source src="ws://example.com/updates"></turbo-stream-source>
Stimulus is "a JavaScript framework with modest ambitions." It enhances server-rendered HTML using data attributes.
// app/javascript/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["name", "output"]
static values = { greeting: { type: String, default: "Hello" } }
static classes = ["active"]
connect() {
// Called when controller connects to DOM
}
disconnect() {
// Called when controller disconnects - clean up here
}
greet() {
this.outputTarget.textContent = `${this.greetingValue}, ${this.nameTarget.value}!`
}
}
| Context | Convention | Example |
|---|---|---|
| Controller file | snake_case or kebab-case | hello_controller.js, date-picker_controller.js |
| Controller identifier | kebab-case in HTML | data-controller="date-picker" |
| Targets | camelCase | static targets = ["firstName"] |
| Values | camelCase | static values = { itemCount: Number } |
| Methods | camelCase | submitForm() |
Define elements to reference within controller:
static targets = ["query", "results", "errorMessage"]
Generated properties:
| Property | Purpose |
|---|---|
this.queryTarget | First matching element (throws if missing) |
this.queryTargets | Array of all matching elements |
this.hasQueryTarget | Boolean existence check |
Target callbacks:
queryTargetConnected(element) {
// Called when target is added to DOM
}
queryTargetDisconnected(element) {
// Called when target is removed from DOM
}
<div data-controller="search">
<input data-search-target="query">
<div data-search-target="results"></div>
</div>
Read/write typed data attributes:
static values = {
index: Number, // data-controller-index-value
url: String, // data-controller-url-value
active: Boolean, // data-controller-active-value
items: Array, // data-controller-items-value (JSON)
config: Object // data-controller-config-value (JSON)
}
Five supported types: Array, Boolean, Number, Object, String
Default values:
static values = {
count: { type: Number, default: 0 },
url: { type: String, default: "/api" }
}
Change callbacks:
countValueChanged(value, previousValue) {
// Called on initialize and when value changes
this.element.textContent = value
}
Connect DOM events to controller methods:
Format: event->controller#method
<button data-action="click->gallery#next">Next</button>
Event shorthand (common defaults):
<button>, <a> → click<form> → submit<input>, <textarea> → input<select> → change<details> → toggle<%# Equivalent to click->gallery#next %>
<button data-action="gallery#next">Next</button>
Global events:
<div data-action="resize@window->gallery#layout">
<div data-action="keydown@document->modal#close">
Keyboard filters:
<input data-action="keydown.enter->search#submit">
<input data-action="keydown.esc->modal#close">
<input data-action="keydown.ctrl+s->form#save">
Action options:
| Option | Effect |
|---|---|
:stop | Calls stopPropagation() |
:prevent | Calls preventDefault() |
:self | Only fires if target matches element |
:capture | Use capture phase |
:once | Remove after first invocation |
:passive | Passive event listener |
<form data-action="submit->form#save:prevent">
<a data-action="click->link#track:stop">
Action parameters:
<button data-action="item#delete"
data-item-id-param="123"
data-item-type-param="article">
delete(event) {
const { id, type } = event.params
// id = 123 (Number), type = "article" (String)
}
Define CSS class names as controller properties:
static classes = ["active", "loading"]
<div data-controller="toggle"
data-toggle-active-class="bg-blue-500"
data-toggle-loading-class="opacity-50">
this.activeClass // "bg-blue-500"
this.hasActiveClass // true
this.loadingClasses // ["opacity-50"]
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin_all_from "app/javascript/controllers", under: "controllers"
bin/importmap pin lodash
bin/importmap pin chartkick --from jsdelivr
references/stimulus-patterns.md - Common Stimulus controller patternsreferences/turbo-streams-advanced.md - Complex broadcasting scenariosThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.