Skip to main content

Detecting if an event was triggered by a user or by JavaScript

By Paul Hebert

Published on May 3rd, 2023

Topics

On a recent project, I finally found a solution to an issue I’ve run into several times: When listening for events in JavaScript, how can I tell whether an event was triggered directly by a user or by my code?

I was enhancing a video element to run a special action whenever a user played the video:

const myVideo = document.querySelector('#my-video');

myVideo.addEventListener('play', () => {
  doSomething();
});Code language: JavaScript (javascript)

This worked great! When a user played the video, my code was able to respond. I was ready to call it a day and go sit in the garden, but there were other enhancements we needed to make.

I needed to be able to programmatically play the video when a user performed certain actions. I wired that up:

function playVideo() {
  // If the video's already playing, do nothing
  if (!myVideo.paused) {
    return;
  }

  myVideo.play().catch((error) => console.warn(error));
}Code language: JavaScript (javascript)

This also seemed to be working great! I got my sun hat to head outside… but then I realized there was an issue. When the play function was called, it triggered my event listener. But I only wanted to run my callback when the user manually played the video, not when my code triggered the play function.

How could I differentiate a user playing the video from my code calling the video play function?

I’d been here before and knew this was a tricky problem. JavaScript doesn’t provide an easy way to distinguish events that are triggered by a user or triggered by code. I started researching, chatting with colleagues, and experimenting. At first, all I found were dead ends.

If you’re not interested in the attempts that didn’t work, you can skip ahead to the working solution.

The first stop on my Dead End World Tour™ was the event.isTrusted property. MDN says the following about this property:

The isTrusted read-only property of the Event interface is a boolean value that is true when the event was generated by a user action, and false when the event was created or modified by a script or dispatched via EventTarget.dispatchEvent().

This sounds like exactly what I needed! I can use it to tell if the event was generated by user action! But, my testing told another story… isTrusted was true whether a user pressed the “Play” button or my code ran myVideo.play()

Looking at the official spec made this clearer:

isTrusted is a convenience that indicates whether an event is dispatched by the user agent (as opposed to using dispatchEvent()).

isTrusted is only false if the event was dispatched using dispatchEvent or a similar function.

Some Stack Overflow posts suggested checking for special properties that would be present for user-triggered events. For example, pointer events have screenX and screenY properties that tell you where the click occurred. If those are both 0 you could be pretty sure that it wasn’t a user-triggered click event.

Unfortunately, I couldn’t find any similar properties to use for the play event.

This raised an obvious question. Could we hook into click events instead? This also didn’t work out. The video element embeds the browser’s video player widget, which captures click events which means they don’t bubble up to my event listener.

I mean… I guess…

In theory, we could have built our own custom video player UI and had greater control over the experience. But this would have greatly increased the development complexity, as well as requiring users to download more JavaScript. We’d also need to reproduce all of the browser’s functionality or risk introducing accessibility issues.

This might have been the right solution for another project, but it felt like overkill here.

I finally found a Stack Overflow post by Ankit Chaudhary that pointed me in the right direction. There’s nothing built-in to JavaScript to help us know whether an event was triggered by a user, but we can add logic to keep track of that ourselves:

const myVideo = document.querySelector('#my-video');

let videoPlayedByCode = false;

function playVideo() {
  // If the video's already playing, do nothing
  if (!myVideo.paused) {
    return;
  }

  // Record that the video playing was triggered by code
  videoPlayedByCode = true;
  myVideo.play().catch((error) => console.warn(error));
}

myVideo.addEventListener('play', () => {
  // If this event was triggered by code, return early and don't
  // perform our actions.
  if (videoPlayedByCode) {
    // But make sure to reset this variable for the next 
    // time the video plays.
    videoPlayedByCode = false;
    return;
  }

  doSomething();
});
Code language: JavaScript (javascript)

This can be a little confusing at first glance. Here are a couple of different scenarios and how this code would handle it.

A user-triggered event:

  1. A user presses “Play.”
  2. Our play event listener is triggered.
  3. videoPlayedByCode is false so our listener proceeds to respond to the user’s action.

A code-triggered event:

  1. Our code calls the playVideo() function.
  2. This function sets videoPlayedByCode to true and then plays the video.
  3. Our play event listener is triggered.
  4. videoPlayedByCode is true so our listener knows this wasn’t a user-triggered action.
  5. videoPlayedByCode is reset to false.

Try playing the video below using the browser’s built-in play button and the custom play button to see how the demo responds.

See the Pen User vs. Code Events by Paul Hebert (@phebert) on CodePen.

As you can see, the browser doesn’t provide much help when differentiating user-triggered events and code-triggered events, but with a bit of custom JavaScript you can keep track yourself. I’ve run into this situation a few times and am happy to finally have a solution. I hope this helps you out if you run into a similar challenge.