Stimulus Discourse

Best practices for handling clicks outside element

I often find myself wanting to execute a particular action when the user clicks outside of an element. For example when dismissing a modal window.

Right now I use something along these lines:

<!-- gets run on ANY click ANYWHERE -->
<div data-action="click@window->myController#myActionIfOutsideClick"></div>

// Stimulus action
myActionIfOutsideClick(event) {

  // Ignore event if clicked within element
  if(this.element === || this.element.contains( return;

  // Execute the actual action we're interested in

This works, but tightly couples the action to the event. Which I think is against the spirit of Stimulus?

Anytime I want to handle the “click outside element” event, I need to make sure I have an additional action that checks the user isn’t clicking on the element itself.

I wonder how others approach this and whether it’s worth considering a custom “clicked outside element” event to the Stimulus library? So we could do something like this instead:

<div data-action="click-outside@window->myController#myAction"></div>

1 Like

I think your solution is a good one.

Thanks for raising this question I used it as an excuse to add a new behavior to stimulus-use to handle similar cases.
This approach is slightly different as it removes the markup requirements from your HTML.

1 Like

Hello there!

I use the same approach for my dropdowns (button + dropdown popup).


<div data-controller="dropdown">
        data-action="click->dropdown#toggle click@window->dropdown#hide touchend@window->dropdown#hide"
        Open dropdown popup
    <div class="is-hidden" data-target="dropdown.popup">
        Dropdown popup's content


import { Controller } from 'stimulus';

export default class extends Controller {
    static targets = [ 'button', 'popup' ]

    toggle(event) {

        if (this.buttonTarget.getAttribute('aria-expanded') == "false") {
        } else {

    show() {
        this.buttonTarget.setAttribute('aria-expanded', 'true');

    hide(event) {
        if (event && (this.popupTarget.contains( || this.buttonTarget.contains( {
            // event.preventDefault(); // I don't remeber why I did it, but i need this line to be commented

        this.buttonTarget.setAttribute('aria-expanded', 'false');

I suggest you to add an addition event listener touchend@window->dropdown#hide to catch outside clicks on some mobile devices.


Thanks Adrien. This is really helpful. I appreciate you spending your time on this.

If I understand your approach correctly, this would require us to use an action called clickOutside, right?

That means we’ll be declaring all behavior in the clickOutside action, rather than a data-action attribute which is typically the case in Stimulus. (e.g. data-target="clickOutside->modal#close")

It does seem very pragmatic though. I tried building a custom “clickOutside” event so we could bind the target/action as above, but I couldn’t get it to work.

hey @marckohlbrugge

FYI I released a new version of Stimulus Use and you now have a click outside mixin available

import { Controller } from 'stimulus'
import { useClickOutside } from 'stimulus-use'

export default class extends Controller {

  connect() {

  clickOutside(event) {
    // example to close a modal
1 Like

Awesome! Thanks for sharing. The custom event is exactly what I was looking for.