Click Events Have Layers
The click event is deceptively simple. You attach a listener, the user interacts with an element, and your code runs. It works on nearly every HTML element and is a core part of the DOM API.
But under that simplicity are nuances that matter when you need more control. The most interesting one? A “click” on a <button> isn’t always caused by a mouse. Pressing Space or Enter while the button has focus fires the same click event, natively, with no extra JavaScript required. That’s a legacy of keyboard navigation that predates modern accessibility concerns, but it remains a useful feature. The catch is that the button only knows a click happened, not how it happened.
That distinction can matter for styling, accessibility, or specific functionality. Here’s how to tell the difference.
Telling Keyboard from Mouse Apart
One straightforward approach: skip the click event and listen for keyup and mouseup instead. Each event tells you its source directly.
// Assume buttonEl is a reference to a <button> element
const output = document.querySelector('.output'); // element for display
buttonEl.addEventListener('mouseup', event => {
if (event.button === 0) {
output.textContent = 'Mouse Up: primary mouse button';
}
});
buttonEl.addEventListener('keyup', event => {
if (event.code === 'Space' || event.code === 'Enter') {
output.textContent = 'Key Up: ' + event.code;
}
});
This requires filtering events. The mouseup event fires for any mouse button, so check event.button === 0 to confirm the primary button. The keyup event fires for any key, so check event.code for Space or Enter.
The code is functional but verbose. If you only want to know “keyboard vs. mouse,” there’s a cleaner approach using the click event’s own properties.
The detail Property
The UI Events specification assigns a detail property to click events tied to mouse input. When the mouse triggers the click, detail is 1 (or higher, representing multiple clicks within the OS double-click threshold). When something else causes it—like the keyboard—detail is 0.
// Assume buttonEl is a reference to a <button> element
const output = document.querySelector('.output'); // element for display
buttonEl.addEventListener('click', event => {
if (event.detail === 0) {
output.textContent = 'Keyboard Click';
} else {
output.textContent = 'Mouse Click';
}
});
This is concise and consistent across modern browsers. It doesn't tell you whether Space or Enter was used, but that’s rarely needed when the distinction is keyboard versus pointer.
Pointer Events for More Granular Input
What if you need to know which pointer device caused the interaction? Mouse, pen, or touch? The click event doesn’t expose that data reliably. Chrome has sourceCapabilities.firesTouchEvents, but Firefox and Safari lack support.
Pointer Events fill that gap. The pointerType property on events like pointerup identifies the device: "mouse", "pen", or "touch". Pointer events also carry richer data about pressure, tilt, and contact size, and they’re supported broadly, even in IE11.
// Assume buttonEl is a reference to a <button> element
const output = document.querySelector('.output'); // element for display
buttonEl.addEventListener('pointerup', event => {
output.textContent = 'Pointer type: ' + event.pointerType;
});
But there’s a catch: using only pointerup drops the keyboard interaction entirely. The click event still fires when the button is activated via keyboard, but nothing is listening for it. To handle both cases, you need to combine events and filter carefully.
// Assume buttonEl is a reference to a <button> element
const output = document.querySelector('.output'); // element for display
buttonEl.addEventListener('pointerup', event => {
output.textContent = 'Pointer type: ' + event.pointerType;
});
buttonEl.addEventListener('click', event => {
if (event.detail === 0) {
output.textContent = 'Keyboard Click';
}
});
The detail property is key here, making sure the mouse click isn’t double-counted—since pointerup handles it—while keyboard activation still works.
Streamlining the Handler
You can reduce this to a single handler if both events call the same function:
function handleInteraction(event) {
const output = document.querySelector('.output');
if (event.type === 'pointerup') {
// handle pointer events
output.textContent = 'Pointer type: ' + event.pointerType;
} else if (event.type === 'click' && event.detail === 0) {
// handle keyboard interactions
output.textContent = 'Keyboard activation';
}
}
buttonEl.addEventListener('pointerup', handleInteraction);
buttonEl.addEventListener('click', handleInteraction);
This checks if the mouse caused the click first—if detail is above zero, it’s ignored in favor of the pointerup event. If detail is zero, it’s keyboard input. The result is cleaner than managing separate listeners but still clear about what’s happening.
Applying This to Other Form Elements
Buttons aren't the only elements with these quirks. Checkboxes, for example, behave differently depending on how you structure your HTML and which events you track.
For a checkbox with a companion <label> linked via the for attribute, clicking the label fires a click event on the label and on the checkbox. That’s two events even though there was one user action. With pointerup, clicking the label produces only one event, since the browser’s auto-generated click on the checkbox isn’t being listened for.
If you wrap the checkbox inside the label element—a common pattern for custom-styled checkboxes—you can put one listener on the label instead of separate ones on each element. Click events still fire twice when you click the checkbox itself, but pointerup stays single.
Checking which input type triggered an interaction then comes down to filtering by target:
const container = document.querySelector('.checkbox-container'); // wrapper element
container.addEventListener('pointerup', (event) => {
if (event.target.closest('label') || event.target.closest('input')) {
// pointer in use, or use event.pointerType for more detail
}
});
container.addEventListener('keyup', (event) => {
if (event.code === 'Space' && event.target.closest('label, input')) {
// keyboard interaction with the checkbox
}
});
Radio buttons follow the same pattern as checkboxes since the underlying structure is nearly identical. The difference is that radios come in groups, but the interaction handling remains the same.
Also worth noting: checkboxes and radio buttons generally respond to Space for toggling, but not Enter, in most browsers.
Nuance as a Tool, Not a Problem
These aren’t bugs to fight—just behaviors to account for. Native keyboard activation on buttons and form controls remains a fundamental accessibility feature and should be preserved even when you add pointer-targeted logic for richer input handling. The techniques above let you distinguish input sources without breaking the standard keyboard experience.
The patterns extend beyond buttons and form fields. Any custom interactive element—such as a rebuilt <select> —will eventually surface these same questions. Knowing how click and detail behave across different input types helps you keep those custom components accessible and consistent.



