.claude_37signals/skills/turbo-patterns/SKILL.md
Creates Turbo Streams, Turbo Frames, and morphing patterns for real-time UI updates. Use when adding real-time updates, partial page rendering, form submissions, or broadcasting. WHEN NOT: For Stimulus JavaScript controllers (see stimulus-patterns skill). For general view conventions (see rules/views.md).
npx skillsauth add ThibautBaissac/rails_ai_agents turbo-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert Hotwire/Turbo architect specializing in building reactive UIs without JavaScript frameworks.
Turbo is plenty. No React, Vue, or Alpine needed. Turbo Streams + Turbo Frames + morphing = rich, reactive UIs with standard Rails views.
Tech Stack: Rails 8.2 (edge), Turbo 8+, Stimulus (for sprinkles), Solid Cable (WebSockets) Pattern: Server-rendered HTML, Turbo for updates, Stimulus for interactions Broadcasting: Database-backed via Solid Cable (no Redis)
curl -H "Accept: text/vnd.turbo-stream.html" http://localhost:3000/cardsbin/dev (starts Rails + CSS/JS build)bin/rails test:systemturbo_stream.append "cards", partial: "cards/card", locals: { card: @card }
turbo_stream.prepend "cards", partial: "cards/card", locals: { card: @card }
turbo_stream.replace @card, partial: "cards/card", locals: { card: @card }
turbo_stream.update @card, partial: "cards/card_content", locals: { card: @card }
turbo_stream.remove @card
turbo_stream.before @card, partial: "cards/new_card_form"
turbo_stream.after @card, partial: "cards/comment", locals: { comment: @comment }
# Bonus: morph (smart replacement, preserves focus/scroll/state)
turbo_stream.morph @card, partial: "cards/card", locals: { card: @card }
| Scenario | Use |
|----------|-----|
| Partial page update from user action | Turbo Stream response |
| Lazy-load content on scroll/visibility | Turbo Frame with loading: :lazy |
| Inline editing | Turbo Frame wrapping show/edit views |
| Real-time update for other users | Turbo Stream broadcast via model |
| Complex update preserving form state | turbo_stream.morph |
| Full page with smooth transition | Turbo Drive (default) |
| Modal/dialog | Turbo Frame with named target |
class Cards::CommentsController < ApplicationController
def create
@comment = @card.comments.create!(comment_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to @card }
end
end
def destroy
@comment = @card.comments.find(params[:id])
@comment.destroy!
respond_to do |format|
format.turbo_stream
format.html { redirect_to @card }
end
end
end
<%# app/views/cards/comments/create.turbo_stream.erb %>
<%= turbo_stream.prepend "comments", partial: "cards/comments/comment", locals: { comment: @comment } %>
<%= turbo_stream.update dom_id(@card, :new_comment), partial: "cards/comments/form", locals: { card: @card } %>
<%= turbo_stream.update dom_id(@card, :comment_count) do %>
<%= pluralize(@card.comments.count, "comment") %>
<% end %>
<%= turbo_stream.prepend "flash" do %>
<div class="flash flash--notice">Comment added</div>
<% end %>
Use turbo_stream.morph instead of replace when the element has form inputs, scroll position, or Stimulus controller state to preserve.
<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">
<div id="<%= dom_id(@card) %>" data-turbo-permanent>
<%# Persists across page loads %>
</div>
# app/controllers/concerns/turbo_flash.rb
module TurboFlash
extend ActiveSupport::Concern
private
def turbo_notice(message)
turbo_stream.prepend "flash", partial: "shared/flash",
locals: { type: :notice, message: message }
end
end
<%= form_with model: @card, data: { turbo_frame: "_top" } %> <%# Full page %>
<%= link_to "Edit", edit_path, data: { turbo_frame: "_self" } %> <%# Current frame %>
<%= link_to "New", new_path, data: { turbo_frame: "modal" } %> <%# Named frame %>
turbo_frame_tag "stats", src: path, loading: :lazy# Controller test
test "create returns turbo stream" do
post card_comments_path(@card),
params: { comment: { body: "Test" } },
as: :turbo_stream
assert_response :success
assert_equal "text/vnd.turbo-stream.html", response.media_type
assert_match /turbo-stream/, response.body
end
# System test
test "creating a comment" do
visit card_path(@card)
fill_in "Body", with: "Great card!"
click_button "Add Comment"
assert_text "Great card!" # Turbo Stream inserts without reload
end
dom_id for element IDs, provide fallback HTML responses, test Turbo responsesturbo_stream_from subscriptionsreferences/turbo-streams.md -- All stream action examples, custom actions, multiple responsesreferences/turbo-frames.md -- Frame patterns, lazy loading, navigation, nested framesreferences/broadcasting.md -- Model broadcasts, ActionCable setup, Solid Cable, channel patternstesting
Writes Minitest tests with fixtures following 37signals conventions. Uses Minitest (not RSpec) and fixtures (not factories). Use when writing tests, adding test coverage, or creating fixtures. WHEN NOT: For RSpec or FactoryBot patterns (this project uses Minitest + fixtures exclusively). For test configuration/CI setup (see project docs).
tools
Builds focused, single-purpose Stimulus controllers for progressive enhancement. Use when adding JavaScript behavior, UI interactions, form enhancements, or building reusable client-side components. WHEN NOT: For Turbo Stream/Frame patterns (see turbo-patterns skill). For server-side view logic (see rules/views.md).
testing
Implements the state-as-records-not-booleans pattern for rich state tracking. Use when modeling state changes, replacing boolean flags with record-based state, or when user mentions state records, closures, publications, or toggling state. WHEN NOT: Technical flags like cached/processed (use booleans), concern extraction (use concern-patterns), general model work (use model-patterns).
data-ai
Implements URL-based multi-tenancy with account scoping, membership patterns, and data isolation following 37signals patterns. Use when setting up multi-tenant architecture, account isolation, membership management, or when user mentions multi-tenancy, accounts, or tenant separation. WHEN NOT: For basic model setup without tenancy (use model-patterns), for auth/session setup (use auth-setup).