Stimulus Actions — Complete Guide with Examples
In this tutorial, you'll learn about Stimulus Actions. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Stimulus actions bind DOM events to controller methods through declarative data-action attributes, replacing addEventListener with readable HTML annotations.
What You'll Learn
- Action descriptors:
event->controller#method - Default events for common HTML elements
- Event options:
preventDefault,stopPropagation,once,passive - Keyboard events and key filtering
- Global events on
windowanddocument - Multiple actions on one element
- Practical patterns for forms, buttons, and keyboard shortcuts
Why It Matters
Manual event binding with addEventListener scatters event logic across JavaScript files, making it hard to see what a button does without reading code. Actions keep event wiring in the HTML, making interactive behavior visible and declarative. In the Doda Browser extension, actions power button clicks, keyboard shortcuts for navigation, form submissions, and global event handling for extension panels.
Learning Path
flowchart LR A[Targets] --> B[Actions] B --> C[Values] C --> D[Classes] B --> E[Lifecycle &
State] style B fill:#4f46e5,color:#fff,stroke:#4f46e5,stroke-width:2px style E fill:#059669,color:#fff
Action Descriptors
The basic syntax is event->controller#method:
<button data-action="click->hello#greet">Greet</button>
| Part | Meaning |
|---|---|
click |
DOM event name |
hello |
Controller identifier |
greet |
Method to call |
Default Events
If you omit the event name, Stimulus uses the element's default event:
<!-- Explicit: click is the default for buttons -->
<button data-action="click->hello#greet">Greet</button>
<!-- Implicit: same as above -->
<button data-action="hello#greet">Greet</button>
Default events by element type:
| Element | Default Event |
|---|---|
<button>, <input>, <select>, <textarea> |
click |
<a href="..."> |
click |
<form> |
submit |
<details> |
toggle |
app.register('hello', class extends Controller {
greet(event) {
console.log('Greeting!', event.target);
event.currentTarget.textContent = 'Clicked!';
}
});
Teacher explains: The event parameter is the native DOM event object. You can access event.target (the element that triggered the event) and event.currentTarget (the element with the data-action attribute).
Event Options
Stimulus supports modifier options for finer control:
preventDefault — Prevent Default Behavior
<!-- Prevent form submission -->
<form data-action="submit->form#handleSubmit">
<!-- ... -->
</form>
<!-- Using preventDefault modifier -->
<form data-action="submit->form#handleSubmit:prevent">
<!-- ... -->
</form>
app.register('form', class extends Controller {
handleSubmit(event) {
// With :prevent modifier, default is already prevented
console.log('Form submitted, but page did not reload');
this.processFormData(new FormData(event.target));
}
});
All Event Modifiers
| Modifier | Effect |
|---|---|
:prevent |
Calls event.preventDefault() |
:stop |
Calls event.stopPropagation() |
:self |
Only fires if event.target === event.currentTarget |
:once |
Only fires once, then removes itself |
:passive |
Sets { passive: true } for scroll performance |
<button data-action="click->notification#alert:prevent:stop">
Alert (prevents default AND stops propagation)
</button>
<div data-action="click->outer#handle:once">
This only fires once
</div>
Keyboard Events
Basic Keyboard Actions
<input type="text" data-action="keydown->search#handleKeydown">
app.register('search', class extends Controller {
handleKeydown(event) {
console.log(`Key pressed: ${event.key}`);
if (event.key === 'Enter') {
this.performSearch();
}
}
});
Key-Specific Filters
Stimulus allows filtering by key name using keydown->controller#method:filter:
<!-- Only fires on Enter key -->
<input type="text" data-action="keydown->search#handleEnter:enter">
<!-- Only fires on Escape key -->
<input type="text" data-action="keydown->search#handleEscape:escape">
<!-- Multiple key filters -->
<input type="text" data-action="keydown->search#handleArrow:arrow-up:arrow-down">
app.register('search', class extends Controller {
static targets = ['input', 'results'];
handleEnter() {
console.log('Enter pressed, searching...');
this.performSearch(this.inputTarget.value);
}
handleEscape() {
console.log('Escape pressed, clearing...');
this.inputTarget.value = '';
this.resultsTarget.innerHTML = '';
}
handleArrow(event) {
if (event.key === 'ArrowDown') this.highlightNext();
if (event.key === 'ArrowUp') this.highlightPrevious();
}
performSearch(query) {
// ... search logic
}
});
Teacher explains: Key filters make your actions declarative. Instead of writing if (event.key === 'Enter') inside a generic handler, you declare the key in the HTML. This keeps controller methods focused on one thing.
Global Events
Listen for events on window or document using the @ suffix:
<div data-action="resize@window->layout#handleResize">
<!-- ... -->
</div>
<div data-action="keydown@document->shortcuts#handleKeydown">
<!-- ... -->
</div>
<div data-action="scroll@window->infinite#loadMore:passive">
<!-- ... -->
</div>
app.register('layout', class extends Controller {
handleResize(event) {
console.log(`Window resized to ${window.innerWidth}x${window.innerHeight}`);
this.adjustLayout();
}
});
app.register('shortcuts', class extends Controller {
handleKeydown(event) {
if (event.key === '?' || event.key === '/') {
this.showHelpModal();
}
if (event.key === 'Escape') {
this.closeAllModals();
}
}
showHelpModal() { /* ... */ }
closeAllModals() { /* ... */ }
});
Multiple Actions on One Element
Same Controller, Different Events
<button data-action="mouseenter->tooltip#show mouseleave->tooltip#hide">
Hover me
</button>
Different Controllers
<button data-action="click->analytics#track click->cart#add">
Add to Cart
</button>
Same Event, Different Controllers
Both controllers receive the event. They fire in order of controller registration.
app.register('analytics', class extends Controller {
track(event) {
console.log('Analytics: track click');
}
});
app.register('cart', class extends Controller {
add(event) {
console.log('Cart: add item');
}
});
Action Parameters
You can pass parameters to actions using data-*-param attributes:
<button data-action="click->menu#select"
data-menu-id-param="settings">
Settings
</button>
<button data-action="click->menu#select"
data-menu-id-param="profile">
Profile
</button>
app.register('menu', class extends Controller {
select(event) {
const { id } = event.params;
console.log(`Selected menu item: ${id}`);
this.loadView(id);
}
loadView(viewId) {
console.log(`Loading view: ${viewId}`);
}
});
Teacher explains: Parameters are defined as data-controller-param-name-param="value" and accessed via event.params.name in the action method. The parameter name is converted from kebab-case to camelCase.
Real-world: Keyboard Shortcuts Panel
<div data-controller="shortcuts"
data-action="keydown@document->shortcuts#handleKey">
<div data-shortcuts-target="content">
<h1>Settings Dashboard</h1>
<p>Press ? for help, Escape to close, Ctrl+S to save</p>
</div>
<div data-shortcuts-target="helpModal" class="hidden">
<h2>Keyboard Shortcuts</h2>
<ul>
<li>? — Show help</li>
<li>Escape — Close panel</li>
<li>Ctrl+S — Save</li>
<li>Ctrl+F — Search</li>
</ul>
</div>
</div>
app.register('shortcuts', class extends Controller {
static targets = ['content', 'helpModal'];
handleKey(event) {
if (event.key === '?') {
event.preventDefault();
this.toggleHelp();
}
if (event.key === 'Escape') {
this.closeAll();
}
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
this.save();
}
}
toggleHelp() {
this.helpModalTarget.classList.toggle('hidden');
}
closeAll() {
this.helpModalTarget.classList.add('hidden');
}
save() {
console.log('Saving settings...');
}
});
Common Mistakes
1. Wrong Action Descriptor Format
<!-- ❌ Wrong: missing # before method name -->
<button data-action="click->hello.greet">Wrong</button>
<!-- ❌ Wrong: extra spaces -->
<button data-action="click -> hello # greet">Wrong</button>
<!-- ✅ Correct -->
<button data-action="click->hello#greet">Correct</button>
2. Calling Non-Existent Methods
<button data-action="click->hello#nonexistent">Click</button>
Stimulus will throw an error when the button is clicked because the method doesn't exist.
3. Forgetting Event Modifier Syntax
<!-- ❌ Wrong: colon placement -->
<button data-action="click->form#submit:preventDefault">Wrong</button>
<!-- ✅ Correct: use shorthand -->
<button data-action="click->form#submit:prevent">Correct</button>
4. Not Accessing event.params Correctly
// HTML: data-user-id-param="42"
// ✅ Correct
select(event) {
console.log(event.params.userId); // 42
}
// ❌ Wrong
select(event) {
console.log(event.userId); // undefined
}
5. Using Dispatched Events Without Proper Namespacing
// Dispatch custom events with proper naming
this.dispatch('selected', { detail: { id: 42 } });
// Listen with: data-action="custom:selected->controller#method"
Practice Questions
1. What is the syntax of a Stimulus action descriptor?
event->controller#method. For example, click->hello#greet.
2. What default events does Stimulus use for <button> and <form> elements?
<button> defaults to click, <form> defaults to submit.
3. How do you listen for a keyboard event on window?
Use the @window suffix: data-action="keydown@window->controller#method".
4. How do you pass parameters to an action method?
Use data-controller-param-name-param="value" in HTML and access via event.params.name in JavaScript.
Challenge
Build a keyboard-shortcuts controller that listens for keydown on document and handles: ? to toggle help, Escape to close modals, and arrow keys to navigate between tabs.
FAQ
What's Next
| Topic | Description |
|---|---|
| {{< ref "stimulus-values" >}} | Configure controllers from HTML with typed values and change callbacks |
| {{< ref "stimulus-classes" >}} | Dynamic CSS class mapping through HTML configuration |
| {{< ref "stimulus-outlets" >}} | Cross-controller references with outlet targets |
| JavaScript Events | Review event propagation, delegation, and custom events |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. This actions tutorial powers the keyboard shortcuts and interactive buttons in the Doda Browser extension.
What's Next
Congratulations on completing this Stimulus Actions tutorial! Here's where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro