Skip to main content

Mouse Events in JavaScript

Mouse events are among the most fundamental interactions in web development. Every click, double-click, right-click, and drag involves a sequence of mouse events that your JavaScript code can intercept and respond to. Understanding the full range of mouse event types, the precise order they fire in, which button was pressed, what modifier keys were held, and where exactly the click occurred gives you complete control over mouse-driven interactions.

This guide covers all the core mouse event types, their firing order, the properties available on every mouse event object, the different coordinate systems, and how to prevent unwanted text selection during mouse-driven UI interactions.

Mouse Event Types

JavaScript provides several mouse event types, each firing at a specific moment in the user's interaction with the mouse.

Click Events

const box = document.getElementById("box");

// click: fires on a full press-and-release cycle (left button by default)
box.addEventListener("click", (event) => {
console.log("Click!");
});

// dblclick: fires on two rapid clicks
box.addEventListener("dblclick", (event) => {
console.log("Double click!");
});

// contextmenu: fires on right-click (before the context menu appears)
box.addEventListener("contextmenu", (event) => {
console.log("Right click!");
// event.preventDefault(); // Prevents the browser's context menu
});

Button Press and Release

// mousedown: fires the INSTANT a mouse button is pressed (before release)
box.addEventListener("mousedown", (event) => {
console.log("Mouse button pressed down");
});

// mouseup: fires the INSTANT a mouse button is released
box.addEventListener("mouseup", (event) => {
console.log("Mouse button released");
});

mousedown and mouseup are lower-level events that give you more control than click. They fire for any mouse button (left, middle, right), while click only fires for the left button (or the primary button).

Practical Example: Press and Hold

const button = document.getElementById("volume-up");
let holdTimer = null;

button.addEventListener("mousedown", () => {
// Start increasing volume immediately
increaseVolume();

// Keep increasing while held
holdTimer = setInterval(increaseVolume, 100);
});

// Stop on release: listen on document to catch release even if mouse leaves button
document.addEventListener("mouseup", () => {
clearInterval(holdTimer);
holdTimer = null;
});

function increaseVolume() {
console.log("Volume up!");
}

All Mouse Event Types at a Glance

EventFires When
mousedownA mouse button is pressed down
mouseupA mouse button is released
clickA left-button press-and-release on the same element
dblclickTwo clicks happen in quick succession
contextmenuRight-click (or Ctrl+click on Mac)
mouseoverThe pointer enters an element (bubbles)
mouseoutThe pointer leaves an element (bubbles)
mouseenterThe pointer enters an element (does not bubble)
mouseleaveThe pointer leaves an element (does not bubble)
mousemoveThe pointer moves while over an element

This article focuses on the first five. Moving-related events (mouseover, mouseout, mouseenter, mouseleave, mousemove) are covered in a separate guide.

Event Order: mousedownmouseupclick

Mouse events fire in a very specific, predictable order. Understanding this sequence is essential for building interactions that depend on precise timing.

Single Click Sequence

When the user clicks the left mouse button once:

1. mousedown
2. mouseup
3. click
const box = document.getElementById("box");

box.addEventListener("mousedown", () => console.log("1. mousedown"));
box.addEventListener("mouseup", () => console.log("2. mouseup"));
box.addEventListener("click", () => console.log("3. click"));

Output when user clicks:

1. mousedown
2. mouseup
3. click

Double Click Sequence

When the user double-clicks:

1. mousedown  (first click)
2. mouseup (first click)
3. click (first click)
4. mousedown (second click)
5. mouseup (second click)
6. click (second click)
7. dblclick
box.addEventListener("mousedown", () => console.log("mousedown"));
box.addEventListener("mouseup", () => console.log("mouseup"));
box.addEventListener("click", () => console.log("click"));
box.addEventListener("dblclick", () => console.log("dblclick"));

// Output on double-click:
// mousedown
// mouseup
// click
// mousedown
// mouseup
// click
// dblclick

Notice that dblclick fires after the second full click sequence. This means both click and dblclick handlers fire during a double-click.

Handling Click vs. Double-Click Separately

Since click fires before dblclick, you need a strategy to distinguish them if you want separate behaviors:

let clickTimer = null;

box.addEventListener("click", (event) => {
if (clickTimer) {
// A click was already pending (this is the second click)
clearTimeout(clickTimer);
clickTimer = null;
return; // Let dblclick handle it
}

// Wait to see if a second click comes
clickTimer = setTimeout(() => {
clickTimer = null;
console.log("Single click action");
}, 250); // 250ms threshold
});

box.addEventListener("dblclick", (event) => {
if (clickTimer) {
clearTimeout(clickTimer);
clickTimer = null;
}
console.log("Double click action");
});
tip

The delay-based approach above introduces a 250ms lag on single clicks. If you can, design your UI so that single-click and double-click perform compatible actions (e.g., single click selects an item, double click opens it). This avoids the timing complexity entirely.

Right-Click Sequence

When the user right-clicks:

1. mousedown  (button === 2)
2. mouseup (button === 2)
3. contextmenu

Note that click does not fire for right-clicks. Only mousedown, mouseup, and contextmenu fire.

box.addEventListener("mousedown", (e) => console.log(`mousedown, button: ${e.button}`));
box.addEventListener("mouseup", (e) => console.log(`mouseup, button: ${e.button}`));
box.addEventListener("click", () => console.log("click"));
box.addEventListener("contextmenu", () => console.log("contextmenu"));

// Right-click output:
// mousedown, button: 2
// mouseup, button: 2
// contextmenu
// (No "click" event!)

Mouse Button: event.button and event.buttons

Mouse events provide two properties for button information: event.button (which button triggered the event) and event.buttons (which buttons are currently held down).

event.button: Which Button Was Pressed

The button property indicates which single button caused the event:

ValueButton
0Left button (primary)
1Middle button (wheel click)
2Right button (secondary)
3Back button (mouse button 4)
4Forward button (mouse button 5)
document.addEventListener("mousedown", (event) => {
switch (event.button) {
case 0:
console.log("Left button pressed");
break;
case 1:
console.log("Middle button (wheel) pressed");
break;
case 2:
console.log("Right button pressed");
break;
case 3:
console.log("Back button pressed");
break;
case 4:
console.log("Forward button pressed");
break;
}
});

event.buttons: Which Buttons Are Currently Held

While event.button tells you which button caused the event, event.buttons tells you which buttons are currently pressed at the time of the event. It is a bitmask:

Bit ValueButton
1 (bit 0)Left button
2 (bit 1)Right button
4 (bit 2)Middle button
8 (bit 3)Back button
16 (bit 4)Forward button

Multiple buttons can be pressed simultaneously, and the values are combined:

document.addEventListener("mousemove", (event) => {
const leftPressed = (event.buttons & 1) !== 0;
const rightPressed = (event.buttons & 2) !== 0;
const middlePressed = (event.buttons & 4) !== 0;

if (leftPressed) console.log("Left button is held down");
if (rightPressed) console.log("Right button is held down");
if (middlePressed) console.log("Middle button is held down");
});

// Example: detect left + right buttons held simultaneously
document.addEventListener("mousemove", (event) => {
if (event.buttons === 3) { // 1 (left) + 2 (right)
console.log("Both left and right buttons are held!");
}
});

event.button vs event.buttons

document.addEventListener("mousedown", (event) => {
console.log(`button (which triggered): ${event.button}`);
console.log(`buttons (all pressed): ${event.buttons}`);
});

// If you press left first, then right (while holding left):
// First mousedown:
// button: 0 (left triggered it)
// buttons: 1 (left is held)

// Second mousedown:
// button: 2 (right triggered it)
// buttons: 3 (left + right are held, 1 + 2)

Practical Example: Custom Context Menu

const element = document.getElementById("workspace");

element.addEventListener("contextmenu", (event) => {
event.preventDefault(); // Prevent default browser menu

const menu = document.getElementById("custom-menu");
menu.style.display = "block";
menu.style.left = `${event.pageX}px`;
menu.style.top = `${event.pageY}px`;
});

// Close custom menu on any click
document.addEventListener("click", () => {
document.getElementById("custom-menu").style.display = "none";
});

Middle-Click Handling

document.addEventListener("mousedown", (event) => {
if (event.button === 1) {
event.preventDefault(); // Prevent auto-scroll on middle click
console.log("Middle-click detected- custom action");
}
});

Modifier Keys

Every mouse event includes information about which keyboard modifier keys were held during the click. These are boolean properties on the event object.

The Four Modifier Properties

document.addEventListener("click", (event) => {
console.log("Shift:", event.shiftKey); // true if Shift was held
console.log("Ctrl:", event.ctrlKey); // true if Ctrl was held
console.log("Alt:", event.altKey); // true if Alt was held
console.log("Meta:", event.metaKey); // true if Meta was held (⌘ on Mac, ⊞ on Windows)
});

Practical Example: Multi-Select

const list = document.getElementById("item-list");
const selectedItems = new Set();

list.addEventListener("click", (event) => {
const item = event.target.closest(".list-item");
if (!item) return;

if (event.ctrlKey || event.metaKey) {
// Ctrl+Click (or ⌘+Click on Mac): toggle individual item
if (selectedItems.has(item)) {
selectedItems.delete(item);
item.classList.remove("selected");
} else {
selectedItems.add(item);
item.classList.add("selected");
}
} else if (event.shiftKey) {
// Shift+Click: select range
console.log("Range select from last selected to this item");
// Implementation depends on your list structure
} else {
// Plain click: select only this item
selectedItems.forEach(i => i.classList.remove("selected"));
selectedItems.clear();
selectedItems.add(item);
item.classList.add("selected");
}
});

Platform-Aware Modifier Handling

On macOS, users expect Cmd (⌘, metaKey) instead of Ctrl for most shortcuts. A common pattern is to check for both:

function isPrimaryModifier(event) {
// On Mac, the primary modifier is ⌘ (metaKey)
// On Windows/Linux, the primary modifier is Ctrl (ctrlKey)
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
return isMac ? event.metaKey : event.ctrlKey;
}

document.addEventListener("click", (event) => {
if (isPrimaryModifier(event)) {
console.log("Primary modifier + click (Cmd on Mac, Ctrl on others)");
}
});

Detecting Modifier Combinations

document.addEventListener("click", (event) => {
if (event.ctrlKey && event.shiftKey) {
console.log("Ctrl + Shift + Click");
} else if (event.altKey && event.shiftKey) {
console.log("Alt + Shift + Click");
} else if (event.ctrlKey && event.altKey) {
console.log("Ctrl + Alt + Click");
}
});

Coordinates: clientX/Y, pageX/Y, screenX/Y, offsetX/Y

Every mouse event includes multiple coordinate pairs, each relative to a different reference point. Understanding which to use in each situation is essential.

The Four Coordinate Systems

document.addEventListener("click", (event) => {
console.log(`Client: (${event.clientX}, ${event.clientY})`);
console.log(`Page: (${event.pageX}, ${event.pageY})`);
console.log(`Screen: (${event.screenX}, ${event.screenY})`);
console.log(`Offset: (${event.offsetX}, ${event.offsetY})`);
});

clientX / clientY: Relative to the Viewport

Coordinates relative to the visible area of the browser window (viewport). These do not change when the page is scrolled:

document.addEventListener("click", (event) => {
// Top-left corner of the viewport is (0, 0)
// Stays the same regardless of scroll position
console.log(`Viewport position: (${event.clientX}, ${event.clientY})`);
});

Use when: Positioning fixed elements (tooltips, popups) or anything that should be relative to the visible window, not the document.

// Show a tooltip at the click position (fixed positioning)
element.addEventListener("click", (event) => {
const tooltip = document.getElementById("tooltip");
tooltip.style.position = "fixed";
tooltip.style.left = `${event.clientX + 10}px`;
tooltip.style.top = `${event.clientY + 10}px`;
tooltip.style.display = "block";
});

pageX / pageY: Relative to the Document

Coordinates relative to the entire document, including the scrolled-out area. If the page is scrolled down 500px and you click at the top of the viewport, clientY is 0 but pageY is 500:

document.addEventListener("click", (event) => {
console.log(`Document position: (${event.pageX}, ${event.pageY})`);

// Relationship between client and page coordinates:
// pageX = clientX + window.scrollX
// pageY = clientY + window.scrollY
console.log(`scrollX: ${window.scrollX}, scrollY: ${window.scrollY}`);
});

Use when: Positioning absolutely positioned elements within the document, or tracking click positions that need to account for scrolling.

// Place a marker at the click position (absolute positioning)
document.addEventListener("click", (event) => {
const marker = document.createElement("div");
marker.className = "click-marker";
marker.style.position = "absolute";
marker.style.left = `${event.pageX}px`;
marker.style.top = `${event.pageY}px`;
document.body.appendChild(marker);
});

screenX / screenY: Relative to the Physical Screen

Coordinates relative to the physical monitor screen. These include the browser's chrome (title bar, tabs, bookmarks bar):

document.addEventListener("click", (event) => {
// Top-left corner of the physical screen is (0, 0)
console.log(`Screen position: (${event.screenX}, ${event.screenY})`);
});

Use when: Rarely needed in web development. Useful for multi-monitor setups, positioning popup windows, or analytics that track absolute screen positions.

offsetX / offsetY: Relative to the Target Element

Coordinates relative to the padding edge of the element that received the event (event.target):

const canvas = document.getElementById("canvas");

canvas.addEventListener("click", (event) => {
// (0, 0) is the top-left corner of the canvas element's padding area
console.log(`Position within element: (${event.offsetX}, ${event.offsetY})`);
});

Use when: Drawing on a <canvas>, placing elements within a specific container, or implementing drag interactions within a bounded area.

// Drawing on a canvas
const canvas = document.getElementById("drawing-canvas");
const ctx = canvas.getContext("2d");

canvas.addEventListener("click", (event) => {
// offsetX/Y gives the exact position within the canvas
ctx.beginPath();
ctx.arc(event.offsetX, event.offsetY, 5, 0, Math.PI * 2);
ctx.fill();
});

Visual Comparison

┌──────────────────── Physical Screen ─────────────────────┐
│ │
│ screenX/Y reference point (0,0) ─┐ │
│ ↓ │
│ ┌──── Browser Window ────────────────────────────┐ │
│ │ [Tab1] [Tab2] [Tab3] │ │
│ │ ← → ↻ [URL bar.............] ─ □ ✕ │ │
│ │ ───────────────────────────────────────────── │ │
│ │ │ │
│ │ clientX/Y reference (0,0) ─┐ │ │
│ │ ↓ │ │
│ │ ┌── Viewport ─────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ ┌── Element ───────────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ offsetX/Y (0,0) ─┐ │ │ │ │
│ │ │ │ ↓ │ │ │ │
│ │ │ │ • click │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └──────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ pageX/Y includes scrolled-out area │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

Choosing the Right Coordinate System

NeedUseWhy
Position a fixed tooltipclientX/YStays relative to viewport
Place a marker in the documentpageX/YAccounts for scroll position
Draw on a canvasoffsetX/YRelative to the element
Multi-monitor positioningscreenX/YPhysical screen coordinates
Drag an elementclientX/YConsistent during drag regardless of scroll

Computing Coordinates from Each Other

element.addEventListener("click", (event) => {
// Client to Page
const pageX = event.clientX + window.scrollX;
const pageY = event.clientY + window.scrollY;

// Page to Client
const clientX = event.pageX - window.scrollX;
const clientY = event.pageY - window.scrollY;

// Client to Offset (for a specific element)
const rect = element.getBoundingClientRect();
const offsetX = event.clientX - rect.left;
const offsetY = event.clientY - rect.top;
});

Preventing Selection During Mouse Actions

When users perform mouse-driven interactions like dragging, resizing, or pressing and holding buttons, the browser's default text selection behavior often interferes. Text gets highlighted unintentionally, creating a poor user experience.

The Problem

// A simple drag handler
let isDragging = false;

element.addEventListener("mousedown", (event) => {
isDragging = true;
});

document.addEventListener("mousemove", (event) => {
if (isDragging) {
// Move the element...
// But the browser is ALSO selecting text as the mouse moves!
}
});

document.addEventListener("mouseup", () => {
isDragging = false;
});

Solution 1: Prevent the mousedown Default

Calling preventDefault() on mousedown prevents the browser from starting a text selection:

element.addEventListener("mousedown", (event) => {
event.preventDefault(); // Prevents text selection from starting
isDragging = true;
});

However, this also prevents focusing the element and other default mousedown behaviors (like activating form controls). Use it only on elements where you want to completely control the mouse interaction.

Solution 2: CSS user-select: none

Apply user-select: none to elements that should never be selectable:

.draggable {
user-select: none; /* Prevents text selection */
cursor: grab;
}

.draggable:active {
cursor: grabbing;
}
// No need to preventDefault: CSS handles selection prevention
element.addEventListener("mousedown", (event) => {
isDragging = true;
element.style.cursor = "grabbing";
});

This is generally the cleanest approach because it is declarative and does not interfere with other mouse event behaviors.

Solution 3: Dynamic Selection Prevention During Drag

Apply the selection prevention only during the drag, not permanently:

element.addEventListener("mousedown", (event) => {
isDragging = true;

// Prevent selection during drag
document.body.style.userSelect = "none";

function onMouseUp() {
isDragging = false;
document.body.style.userSelect = "";
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
}

function onMouseMove(moveEvent) {
if (!isDragging) return;
// Handle drag...
element.style.left = `${moveEvent.pageX}px`;
element.style.top = `${moveEvent.pageY}px`;
}

document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});

Solution 4: Handle the selectstart Event

The selectstart event fires when text selection is about to begin. Preventing it stops selection without affecting other mouse behaviors:

element.addEventListener("selectstart", (event) => {
event.preventDefault(); // Prevent selection only on this element
});

Or dynamically:

function preventSelection(event) {
event.preventDefault();
}

element.addEventListener("mousedown", (event) => {
isDragging = true;
document.addEventListener("selectstart", preventSelection);
});

document.addEventListener("mouseup", () => {
isDragging = false;
document.removeEventListener("selectstart", preventSelection);
});

Preventing Double-Click Selection

Double-clicking selects a word by default. Prevent this on interactive elements:

// Prevent double-click selection on buttons and interactive elements
const toolbar = document.getElementById("toolbar");

toolbar.addEventListener("mousedown", (event) => {
// Prevent selection on double-click
if (event.detail > 1) {
event.preventDefault();
}
});

// event.detail contains the click count:
// 1 for single click, 2 for double, 3 for triple, etc.

Clearing an Existing Selection Programmatically

function clearSelection() {
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
}

element.addEventListener("mousedown", () => {
clearSelection(); // Remove any existing selection when starting an interaction
});

Complete Draggable Example

Combining all concepts from this guide:

function makeDraggable(element) {
let offsetX, offsetY;

element.style.userSelect = "none";
element.style.cursor = "grab";
element.style.position = "absolute";

element.addEventListener("mousedown", (event) => {
// Only left button
if (event.button !== 0) return;

// Calculate offset from element's top-left to click position
const rect = element.getBoundingClientRect();
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;

element.style.cursor = "grabbing";
element.style.zIndex = "1000";

function onMouseMove(moveEvent) {
element.style.left = `${moveEvent.pageX - offsetX}px`;
element.style.top = `${moveEvent.pageY - offsetY}px`;
}

function onMouseUp() {
element.style.cursor = "grab";
element.style.zIndex = "";
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
}

document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});

// Prevent drag-related browser defaults
element.addEventListener("dragstart", (event) => {
event.preventDefault();
});
}

makeDraggable(document.getElementById("draggable-box"));

Summary

ConceptKey Takeaway
clickLeft-button press-and-release on the same element
dblclickTwo rapid clicks; fires after the second full click sequence
contextmenuRight-click; fires before the browser's context menu
mousedown / mouseupLower-level events for any button; fire before click
Event ordermousedownmouseupclick (for left button)
event.buttonWhich button triggered the event (0=left, 1=middle, 2=right)
event.buttonsBitmask of all currently pressed buttons
Modifier keysshiftKey, ctrlKey, altKey, metaKey (boolean properties)
clientX/YRelative to the viewport (visible window area)
pageX/YRelative to the full document (includes scroll)
screenX/YRelative to the physical monitor
offsetX/YRelative to the target element's padding edge
Preventing selectionUse CSS user-select: none, preventDefault() on mousedown, or handle selectstart

Mouse events give you granular control over every aspect of mouse interaction. The key is choosing the right event type for your use case (click for simple actions, mousedown/mouseup for drag and hold interactions), the right coordinate system for your positioning needs, and the right selection-prevention strategy for your interactive elements.