Turbo morphing
Unofficial Claude Code skillset inspired by The Rails and Hotwire Codex — 8 skills for building interactive Rails + Hotwire apps (Turbo, Stimulus, Hotwire Native) across web, iOS & Android, each with a runnable checker.
npx -y skills add davidteren/hotwire-codex-skills --skill turbo-morphingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Apply Turbo 8 page refreshes with morphing and broadcast refreshes correctly, and avoid the morphing footguns. Use when adding smooth page refreshes (turbo_refreshes_with / turbo-refresh-method morph), broadcasting live updates with broadcasts_refreshes, deciding between morph-refresh vs targeted Turbo Streams vs frames, or when morphing resets browser state (an open details or dialog element, popovers, scroll, focus, JS widgets) after a refresh. Provides a decision guide and a morph-readiness checker.
SKILL.md
3.7 KB, as published. Nobody here has run it
Turbo 8 morphing & page refreshes
Turbo 8 added a second update model alongside targeted Turbo Streams: page refreshes with morphing. Render the current page again (e.g. submit → redirect back) and Turbo morphs only the changed DOM via idiomorph, preserving screen state. It's the simpler "happy path" — but it has sharp edges. This skill helps you pick the right tool and avoid the footguns.
Full background: references/morphing-guide.md. Real-world note:
piazza-web/wip/analysis/08-turbo8-morphing-refreshes.md.
When to use
- Adding
turbo_refreshes_with method: :morph, scroll: :preserve. - Broadcasting live updates and considering
broadcasts_refreshesvs targeted streams. - Morphing resets UI state (a
<details>reopens for everyone, a popover closes, scroll jumps, a JS-initialized widget breaks) after a refresh/broadcast. - Reviewing a PR that enables morphing, especially in the global layout.
Decision guide
| Need | Use |
|---|---|
| Submit-and-redirect-back with many small scattered changes | morph page refresh: turbo_refreshes_with method: :morph |
| Live multi-user updates, simplest server code, "good enough" fidelity | broadcasts_refreshes + turbo_stream_from @record |
| High-fidelity append/prepend (chat), surgical control | targeted streams (broadcast_append_to, etc.) |
Reload one region during a full-page refresh / explicit .reload() | frame with refresh="morph" |
| Morph a single element instead of replacing it | turbo_stream.replace id, ..., method: :morph |
Mantra (37signals): morphing is an implementation detail of a page refresh, not a new partial-update tool. Use targeted streams when you need precision.
broadcasts_refreshes (the simplification)
class Listing < ApplicationRecord
broadcasts_refreshes # after create/update/destroy → <turbo-stream action="refresh">
end
<%= turbo_stream_from @listing %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
Async via Turbo::Streams::BroadcastJob (needs a job backend), auto-debounced
(only the last of a burst), and self-refreshes are deduped via X-Turbo-Request-Id.
Avoid the footguns (morphing resets browser-only state)
In order of preference:
data-turbo-permanent— exclude an element from morphing entirely.turbo:before-morph-element/turbo:before-morph-attribute+ a small Stimulus controller to veto a specific change (e.g. keep<details open>open).- Best — keep the state on the server (a user-pref record or a URL param like
?sidebar=1) so the re-render already matches the browser; morph is then a no-op.
Don't enable turbo_refreshes_with in the global layout without auditing stateful
widgets first.
Check a repo
scripts/lint_morphing.sh path/to/rails-app
Reports: whether morph is enabled (and if it's global — in the layout — which is
risky), stateful widgets (<details open>, <dialog>, scrollable/JS-controlled
elements) that morphing may reset without a data-turbo-permanent /
before-morph guard nearby, and _later broadcast usage that needs a job backend.
Heuristic grep scan that names its ceiling — guidance, not a parser.