A Wheels package that ships Hotwire infrastructure: Turbo Drive, Turbo Frames, Turbo Streams, Stimulus helpers, and Hotwire Native mobile support. This is the interaction layer — it has zero opinions about CSS or visual design. Pair it with wheels-basecoat, your own design system, or none at all.
- Wheels 3.0+
- Lucee 5+ or Adobe ColdFusion 2018+
# Activate the package
cp -r packages/hotwire vendor/hotwire
# Restart or reload your app
wheels reloadAll turbo*, hotwire*, stimulus*, and is* helpers become available in views and controllers via the package mixin system.
Hotwire has no set()-style application settings. The only helper with knobs is hotwireIncludes():
#hotwireIncludes(
turbo = true,
stimulus = true,
turboVersion = "8",
stimulusVersion = "3",
turboCacheControl = "no-preview"
)#| Argument | Default | Description |
|---|---|---|
turbo |
true |
Include the Turbo ES module (@hotwired/turbo). |
stimulus |
true |
Include the Stimulus ES module and start a global window.Stimulus Application. |
turboVersion |
"8" |
Turbo major version served from jsDelivr. |
stimulusVersion |
"3" |
Stimulus major version served from jsDelivr. |
turboCacheControl |
"no-preview" |
Value for the <meta name="turbo-cache-control"> tag. Pass an empty string to omit the tag. |
<!-- app/views/layout.cfm -->
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
#hotwireIncludes()#
</head>
<body>
#includeContent()#
</body>
</html>Detect the request, then respond with one or more streams using renderTurboStream():
component extends="Controller" {
function create() {
comment = model("Comment").create(params.comment);
if (isHotwireRequest()) {
// `content` is any HTML string — render a partial to a string first,
// or build markup inline as shown here.
renderTurboStream([
turboStreamAppend(target="comments-list", content="<li>#comment.body#</li>"),
turboStreamUpdate(target="comment-count", content="#model('Comment').count()# comments")
]);
return;
}
redirectTo(action="index");
}
}Available stream actions map 1:1 to the Turbo spec: turboStreamAppend, turboStreamPrepend, turboStreamReplace, turboStreamUpdate, turboStreamRemove, turboStreamBefore, turboStreamAfter, turboStreamRefresh. renderTurboStream() sets the text/vnd.turbo-stream.html content type and aborts for you.
<!-- app/views/posts/show.cfm -->
#turboFrame(id="post-#post.id#")#
<h1>#post.title#</h1>
<p>#post.body#</p>
#linkTo(text="Edit", route="editPost", key=post.id)#
#turboFrameEnd()#
<!-- Lazy-loaded frame -->
#turboFrame(id="recent-activity", src=urlFor(controller="activity"), loading="lazy")#
<p>Loading…</p>
#turboFrameEnd()#Place Stimulus controllers in public/controllers/ (or wherever your asset pipeline serves JS from) and register them against the global window.Stimulus application started by hotwireIncludes():
// public/controllers/clipboard_controller.js
import { Controller } from "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3/+esm";
export default class extends Controller {
static targets = ["source"];
copy() { navigator.clipboard.writeText(this.sourceTarget.value); }
}
window.Stimulus.register("clipboard", (await import("/controllers/clipboard_controller.js")).default);Use the Stimulus convenience helpers to build data attributes without string-concat bugs:
<div #stimulusController("clipboard")#>
<input #stimulusTarget(controller="clipboard", name="source")# value="Copy me" />
<button #stimulusAction("click->clipboard##copy")#>Copy</button>
</div>Available helpers: stimulusController, stimulusAction, stimulusTarget, stimulusValue.
When a request comes from a Turbo Native iOS/Android shell, redirect through the special interception paths instead of a normal HTTP redirect. Each helper falls through to redirectTo() on web requests, so the same controller code works for both clients:
function create() {
post = model("Post").create(params.post);
// Native: dismiss/pop. Web: redirect to index.
recedeOrRedirectTo(route="posts");
}
function update() {
post = model("Post").findByKey(params.key);
post.update(params.post);
// Native: refresh current screen. Web: redirect to show.
refreshOrRedirectTo(route="post", key=post.id);
}Available helpers: recedeOrRedirectTo (pops), refreshOrRedirectTo (reloads), resumeOrRedirectTo (no-op). Detect native requests directly with hotwireNativeApp().
Serve a JSON document controlling navigation behavior in native apps:
// In a controller action routed at GET /native/config
function config() {
hotwireNativePathConfiguration(
settings = {
tabs: [
{title: "Home", path: "/dashboard", icon: "house"},
{title: "Orders", path: "/orders", icon: "list.clipboard"}
]
}
// Sensible default rules are applied if `rules` is omitted —
// see hotwireNativePathConfiguration() for the defaults.
);
}| Helper | Returns true when |
|---|---|
isHotwireRequest() |
Accept header contains text/vnd.turbo-stream.html |
isTurboFrameRequest() |
Turbo-Frame request header is present |
turboFrameRequestId() |
Returns the requesting frame's id (or "") |
hotwireNativeApp() |
User-Agent contains Turbo Native |
rm -rf vendor/hotwire
wheels reloadpackages/hotwire/CLAUDE.md— markup reference, helper inventory, naming conventionspackages/hotwire/.ai/ARCHITECTURE.md— detailed architecture notes- Hotwire — upstream documentation for Turbo, Stimulus, and Hotwire Native
MIT