Introduction
Stimulus is the JavaScript framework in Hotwire, designed to add behavior to HTML through controllers. Rails 8 ships Stimulus fully configured through Import Maps — no additional installation needed.
Key Concepts
- Stimulus Controller: A JavaScript class connected to HTML elements via
data-controllerattributes. - Targets: Named references to DOM elements declared with
static targets. - Actions: Event handlers mapped to methods via
data-actionattributes. - Auto-registration:
eagerLoadControllersFromautomatically registers all_controller.jsfiles.
Real World Context
You will use Stimulus controllers for every interactive behavior: toggling menus, form validation, showing/hiding elements, modals, and integrating third-party libraries. Unlike SPA frameworks, Stimulus enhances server-rendered HTML rather than replacing it.
Deep Dive
Default File Structure
Rails 8 generates Stimulus setup automatically:
app/javascript/
├── application.js
└── controllers/
├── application.js
└── hello_controller.js
The shared application instance:
javascript// app/javascript/controllers/application.js import { Application } from "@hotwired/stimulus" const application = Application.start() application.debug = false window.Stimulus = application export { application }
This creates a single Stimulus Application instance. window.Stimulus helps debugging in the console.
Auto-registration
javascript// app/javascript/controllers/index.js import { application } from "controllers/application" import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application)
eagerLoadControllersFrom scans the controllers directory and registers every _controller.js file. Just create the file and it works.
Creating Controllers
bashbin/rails generate stimulus search
This generates:
javascript// app/javascript/controllers/search_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["input", "results"] search() { const query = this.inputTarget.value this.resultsTarget.textContent = `Searching for: ${query}` } }
Targets become properties like this.inputTarget. Connect HTML with data attributes:
html<div data-controller="search"> <input data-search-target="input" data-action="input->search#search"> <p data-search-target="results"></p> </div>
data-controller instantiates the class. data-action maps events to methods. data-*-target creates named references.
Namespaced Controllers
Subdirectories use double-dash notation:
controllers/admin/users_controller.js → data-controller="admin--users"
Common Pitfalls
- Forgetting
_controller.jssuffix — Auto-registration depends on this naming convention. - Wrong data-action syntax — Format is
event->controller#method. Dots and colons won't work.
Best Practices
- Use the generator —
bin/rails generate stimulus nameensures correct naming and boilerplate. - Keep controllers small — One concern per controller. Split at 50-60 lines.
Summary
- Rails 8 ships Stimulus configured with Import Maps.
- Controllers in
app/javascript/controllers/ending with_controller.jsare auto-registered. - HTML connects via
data-controller,data-*-target, anddata-action. - Namespaced controllers use subdirectories and double-dash notation.
Code Examples
// app/javascript/controllers/toggle_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["content"]
static classes = ["hidden"]
toggle() {
this.contentTarget.classList.toggle(this.hiddenClass)
}
}
// <div data-controller="toggle" data-toggle-hidden-class="hidden">
// <button data-action="click->toggle#toggle">Toggle</button>
// <div data-toggle-target="content">Content here</div>
// </div>