Skip to content

Stimulus Actions — Complete Guide with Examples

DodaTech Updated 2026-06-28 7 min read

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 window and document
  • 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

### Can I use multiple actions of the same event type on one element?

Yes. List them separated by space: data-action="click->ctrl#save click->ctrl#log". Both methods will fire in order.

How do I stop an action from firing?

Return false from the method, or use the :stop modifier to call event.stopPropagation(). To prevent default behavior, use :prevent.

What is the difference between event.target and event.currentTarget?

event.target is the element that triggered the event. event.currentTarget is the element where the action is defined (the element with data-action).

Can I use action parameters with global events?

Yes. Parameters work with any action, including global events on window and document.

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