From vanilla-rails
Use when writing Hotwire (Turbo/Stimulus) code in Rails - enforces dom_id helpers, morph updates, focused Stimulus controllers, and JavaScript private methods
How this skill is triggered — by the user, by Claude, or both
Slash command
/vanilla-rails:hotwireThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
37signals Hotwire conventions beyond the official docs.
37signals Hotwire conventions beyond the official docs.
Always dom_id, never string interpolation:
<%# Wrong %>
<%= turbo_stream.replace "card_#{@card.id}" do %>
<%# Right %>
<%= turbo_stream.replace dom_id(@card) do %>
<%= turbo_stream.replace [ @card ] do %>
Prefixed dom_id for granular updates:
dom_id(@card) # "card_abc123"
dom_id(@card, :header) # "header_card_abc123"
dom_id(@card, :status_badge) # "status_badge_card_abc123"
Always method: :morph for replacements (avoids layout shift, preserves scroll):
<%= turbo_stream.replace dom_id(@card, :status), method: :morph do %>
<%= render "cards/status", card: @card %>
<% end %>
Morph for updates. append/prepend for new items. remove for deletions.
One purpose per controller. Split large controllers.
Private methods with # prefix — only methods called from data-action are public:
export default class extends Controller {
#debounceTimer = null // Private field
copy() { // Public - called from data-action
navigator.clipboard.writeText(this.sourceTarget.value)
this.#showNotification()
}
#showNotification() { // Private - internal only
this.element.classList.add('success')
}
}
Public methods: Those in data-action="controller#method" + lifecycle (connect, disconnect, *ValueChanged, *TargetConnected)
Private methods: Everything else — helpers, callbacks, utilities. Add #.
No business logic in Stimulus. Controllers coordinate UI only. Validations and data transforms go in Rails.
Structure partials with prefixed dom_id for targeted updates:
<article id="<%= dom_id(card) %>" class="card">
<div id="<%= dom_id(card, :status) %>">
<%= render "cards/status", card: card %>
</div>
<div id="<%= dom_id(card, :header) %>">
<%= render "cards/header", card: card %>
</div>
</article>
| Red flag | Fix |
|---|---|
"card_#{@card.id}" | dom_id(@card) |
turbo_stream.replace without method: :morph | Add method: :morph |
Helper method without # | Add # prefix |
| One Stimulus controller doing 5+ things | Split into focused controllers |
| Validations in JavaScript | Move to Rails model |
| Animation logic in Stimulus | Use CSS transitions |
npx claudepluginhub zemptime/zemptime-marketplace --plugin vanilla-railsImplements Hotwire Turbo (Drive, Frames, Streams, Morph) and Stimulus controllers in Rails views for SPA-like interactivity, real-time updates, and progressive enhancement.
Creates and refactors Stimulus controllers using Hotwire conventions, design patterns, targets/values, action handling, and JavaScript best practices for interactive UIs.
Guides building reactive Rails apps with Hotwire (Turbo Drive/Frames/Streams, Stimulus): installation, ActionCable/Redis setup, core patterns.