Setting up Rails with Stimulus
Before copying any controller from this site, your Rails app needs Stimulus running. This is usually already true — Rails 7+ includes it by default — but here’s how to check, and how to add it if it’s missing.
New Rails app
rails new has included Stimulus by default since Rails 7, via the stimulus-rails gem and importmap-rails. If you’re starting fresh, there’s nothing to install — skip to Copying a controller.
Existing app: check if Stimulus is already set up
Look for stimulus-rails in your Gemfile:
bundle list | grep stimulus
If that prints stimulus-rails, you’re set — skip to Copying a controller. If not, install it using whichever JavaScript setup your app already uses.
Installing Stimulus (importmap — no Node build step)
This is the Rails 7+ default and the simplest option if your app doesn’t already have a Node-based JS bundler.
bundle add stimulus-rails
bin/rails stimulus:install
This generates app/javascript/controllers/application.js and app/javascript/controllers/index.js, and pins @hotwired/stimulus in config/importmap.rb.
Installing Stimulus (esbuild / webpack / rollup via jsbundling-rails)
If your app already bundles JS with jsbundling-rails (esbuild, webpack, or rollup), install Stimulus through npm/yarn instead so it ends up in the same bundle:
bundle add stimulus-rails
bin/rails stimulus:install
The generator detects jsbundling-rails and adds @hotwired/stimulus to package.json rather than config/importmap.rb. Run your usual install command (yarn install or npm install) afterward.
Verifying the install
Two files matter:
// app/javascript/controllers/application.js
import { Application } from "@hotwired/stimulus";
const application = Application.start();
application.debug = false;
window.Stimulus = application;
export { application };
// app/javascript/controllers/index.js
import { application } from "controllers/application";
// Controllers get registered here, one import + register per controller.
And app/javascript/application.js (or your main JS entrypoint) should load the controllers index:
import "controllers";
If app/javascript/controllers/ doesn’t exist at all, re-run bin/rails stimulus:install — it’s safe to run again.
One more thing to check, and the most common reason nothing happens at all: your layout actually has to render the script tag that loads this entrypoint. On an importmap setup that’s:
<%= javascript_importmap_tags %>
or, on jsbundling-rails:
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
bin/rails stimulus:install adds this automatically on a fresh install, but it’s easy to lose on a hand-rolled or migrated layout — and when it’s missing, there’s no error, Stimulus just never boots.
Copying a controller
Once Stimulus is confirmed working, using any controller from this site is the same three steps regardless of which install path you took:
- Copy the controller file into
app/javascript/controllers/. - Register it in
app/javascript/controllers/index.js. - Add
data-controlleranddata-actionattributes to your HTML.
// app/javascript/controllers/index.js
import { application } from "controllers/application";
import DismissController from "./dismiss_controller";
application.register("dismiss", DismissController);
If you’re on the importmap setup, also pin the file so the browser can resolve the import:
# config/importmap.rb
pin "controllers/dismiss_controller", to: "controllers/dismiss_controller.js"
(jsbundling-rails apps don’t need a pin — the bundler resolves the relative import automatically.)
Smoke-testing it
Drop the controller’s HTML example (each component’s page on this site has one) into any view, then check the browser console:
- No
data-controllerwarnings, and the behaviour described in the component’s README works — you’re done. - A
Failed to autoload controller: dismiss(or similar) message means the registration step was missed, or the importmap pin is missing/misspelled.
Common gotchas
- Naming mismatch.
dismiss_controller.jsregisters asapplication.register("dismiss", ...)and is referenced asdata-controller="dismiss". Stimulus derives the identifier from the registered name, not the filename — if they drift, double-checkindex.js. - Turbo and reconnects. Turbo Drive swaps
<body>content between page visits, which disconnects and reconnects controllers. Most controllers on this site handle that already (state is read fromdata-*attributes onconnect(), not held only in memory) — but if you write your own, keep that pattern in mind. - Importmap cache in production. After pinning a new controller, you don’t need to re-run
bin/rails importmap:install— but you do need a fresh asset precompile (bin/rails assets:precompile) so the new pin ships. - CSP and importmap nonces. Rails ships with
config/initializers/content_security_policy.rbcommented out, but if your app enables it, the inline<script type="importmap">tag needs a nonce.javascript_importmap_tagsadds this automatically — but only oncepolicy.nonceis configured. Apps that enable CSP without checking this getRefused to execute inline scripterrors, often only in production since CSP is commonly left disabled in development.