Stimulus in Practice: Controllers, Actions, and Targets

Stimulus is a small JavaScript library — roughly 30kb — designed to complement existing HTML rather than replace it. It encourages small, reusable "sprinkles" of JavaScript organized so that the HTML itself hints at which code connects to it. When reading a Stimulus-powered page, the connection between markup and behavior reads almost like pseudocode.

Created by the team at Basecamp (the makers of the HEY email service), Stimulus was built primarily to keep the JavaScript in their own web applications maintainable. Given Basecamp's track record of sustaining open-source projects, Stimulus has been thoroughly tested and should remain viable for years to come.

The framework's premise is simple: it lets you build applications in a reusable and approachable way. While it's unlikely to displace React or Vue, it's a worthwhile tool to understand — and one that pairs well with existing markup.

Core Terminology

Unlike many frameworks, Stimulus introduces only two terms you need to master:

  • Controller — An instance of a JavaScript class that acts as a building block for your application. When discussing Stimulus controllers, you're talking about JavaScript classes.
  • Identifier — The name used to reference a controller in HTML via a data attribute, common throughout Stimulus codebases.

Getting Started: Key Concepts

The examples below use the version of Stimulus distributed via unpkg.com so you can run them directly in the browser. In production, you'd typically use a module bundler like webpack — the approach documented in the Stimulus Handbook — which allows for better code organization.

Application Startup

The application.start line tells Stimulus to attach listeners to the page. Calling it once at the top of your page returns an instance of Stimulus's main controller, which includes the register method used to connect classes to the framework.

Controllers

The data-controller attribute connects an HTML element to an instance of a JavaScript class. Using an identifier like counter, you hook up an instance of a corresponding CounterController class to your element. The connection between identifier and controller is established via application.register.

Stimulus continuously monitors the page for elements with this attribute being added or removed. When new HTML with a data-controller attribute appears, Stimulus initializes a new controller instance and connects it to the element. Removing that element triggers the disconnect method on the class.

Actions

Actions are defined using the data-action attribute, with the syntax event->controller#function. Anyone reading the HTML can immediately see what triggers what. For example, when a button fires its click event, it gets passed to the addOne function in the counter controller. This explicit pattern reduces the risk of unexpected behavior originating from other files.

Targets

Targets explicitly define which elements are accessible to a controller. Instead of mixing ID selectors, class names, and data attributes, Stimulus provides a consistent approach: you define target names within the controller class using the targets function, then add the name to an element via data-target.

Once established, the element becomes available in the controller. For instance, a target named output is accessed via this.outputTarget.

When multiple elements share the same target name, the plural accessor returns an array. Calling this.outputTargets would return an array containing all elements with data-target="hello.output" — a useful pattern for iterating over groups of related elements.

Events and Data Binding

Event Types

Stimulus supports any event you'd normally attach with addEventListener: button clicks, form submissions, input changes, and more.

To listen for window or document events — such as resizing or navigating offline — append @window or @document to the event type. For example, resize@window->console#logEvent calls the logEvent function on the console controller when the window is resized.

Stimulus also offers an event shorthand that omits the event type. However, this shorthand is best avoided: it increases the number of assumptions a reader must make about the code, sacrificing the clarity that Stimulus is designed to provide.

Multiple Controllers on One Element

There are times when breaking two pieces of logic into separate classes makes sense, even if their markup lives close together. Stimulus supports this by allowing multiple controller references on a single element.

In practice, you could have a basket controller that counts total items while a separate child controller displays the number of bags per item — each handling its own concern without entanglement.

Passing Data to Controllers

Controllers include the this.data.get and this.data.set methods for reading and writing data attributes within the same namespace as the identifier. To pass data from HTML, add an attribute like data-[identifier]-a-variable to the controller's element.

When calling this.data.set, the value in the HTML updates in real time — visible when inspecting the element in browser developer tools. This namespaced approach makes it clear which data attribute belongs to which piece of code.

Lifecycle Methods and Reuse

Initialize, Connect, Disconnect

As applications grow, hooks into lifecycle events become necessary for setting defaults, fetching data, or handling real-time communication. Stimulus offers three built-in lifecycle methods:

  • initialize — Called when Stimulus creates a new controller instance after encountering a matching data-controller attribute. This typically runs on page load, but also fires when new HTML is appended (e.g., via AJAX). It does not run when an element is repositioned within the DOM.
  • connect — Called after initialization attaches the controller to its HTML element. It also fires if you move an element within the DOM. Moving an element from one parent to another triggers only connect, not initialize.
  • disconnect — Runs when an element is removed from the page. This is useful for tearing down code that relies on the element's position. A WYSIWYG editor that adds extra HTML, for instance, could revert to its original state in disconnect.

Extending Controllers

Sharing functionality between Stimulus controllers is straightforward because controllers are, underneath, essentially plain JavaScript classes. Standard inheritance patterns apply: you can define a parent controller class and extend it with a child class, inheriting methods without duplicating code.

Stimulus in a Real Project

The stand-alone examples above demonstrate the mechanics, but Stimulus really shines when you integrate it into an existing server-rendered application. The library was designed for exactly that workflow: HTML delivered from the server, with JavaScript layered on top to enhance interactions rather than replace the document.

Working With Webpack

Stimulus pairs particularly well with Webpack. The official starter kit shows how to split controllers into separate files, letting Stimulus automatically derive the correct identifier from each filename. Webpack isn't required, but it makes the experience noticeably cleaner—for many developers, Stimulus is the tool that makes Webpack's value concrete.

File Naming and Organization

Once Webpack is configured, Stimulus enforces a naming convention: files follow the pattern [identifier]_controller.js, where the identifier matches what you put in the data-controller attribute. As projects grow, you can organize controllers into subfolders. Stimulus then converts underscores to dashes and folder slashes to double dashes in the identifier. For instance, chat/conversation_item_controller.js becomes chat--conversation-item.

Writing Less JavaScript

The philosophy behind Stimulus aligns with the idea that the best code is no code at all. Browsers are steadily standardizing features that previously required hand-rolled JavaScript—the details element is a good example of something that once demanded jQuery but is now native HTML.

Stimulus encourages building with accessible HTML first and adding only the JavaScript needed for today's interaction, with the understanding that the implementation can be easily replaced when browsers catch up. Writing less code from the start makes that future migration far simpler.

HTML First, JavaScript Second

Stimulus naturally supports sending HTML down to the user first, then enhancing it with JavaScript. This approach gets content in front of users immediately, while the interaction layer initializes in the background. As a bonus, if JavaScript fails for any reason, the content remains visible and usable—a form that would normally submit via AJAX falls back to a traditional page-reloading request.

Final Thoughts

Stimulus fits a specific niche: sites that need small, maintainable sprinkles of JavaScript to enhance the user experience. The learning curve is gentle because there's very little to it, which also means less cognitive overhead when organizing files and tracing how JavaScript connects to HTML. Passing code to another developer feels safer when the conventions are this clear.

Stimulus isn't a competitor to React or Vue—it serves a different purpose. For server-rendered applications where the HTML is the source of truth and JavaScript only improves the interaction, Stimulus is the right tool. For more complex client-side requirements, a different framework would be more appropriate.

Resources

Smashing Editorial