Detecting if an event was triggered by a user or by JavaScript
At 4/19/2024
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?
Dead Ends
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.
Trusted Events
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 theEvent
interface is a boolean value that istrue
when the event was generated by a user action, andfalse
when the event was created or modified by a script or dispatched viaEventTarget.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 usingdispatchEvent()
).
isTrusted
is only false
if the event was dispatched using dispatchEvent
or a similar function.
Are there other event properties we could use?
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.
Could we use a click event instead?
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.
Could we build a custom video player?
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.
The (hacky) solution
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:
- A user presses “Play.”
- Our
play
event listener is triggered. videoPlayedByCode
isfalse
so our listener proceeds to respond to the user’s action.
A code-triggered event:
- Our code calls the
playVideo()
function. - This function sets
videoPlayedByCode
totrue
and then plays the video. - Our
play
event listener is triggered. videoPlayedByCode
istrue
so our listener knows this wasn’t a user-triggered action.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.