Mouse Events
Handle clicks, double-clicks, hovers, and mouse movement in React components.
Search across all documentation pages
Handle clicks, double-clicks, hovers, and mouse movement in React components.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| React Prop | TypeScript Type | Fires When |
|---|---|---|
onClick | React.MouseEvent<T> | Element is clicked (mousedown + mouseup on same target) |
onDoubleClick | React.MouseEvent<T> | Element is double-clicked |
onMouseDown | React.MouseEvent<T> | Mouse button is pressed down on element |
onMouseUp | React.MouseEvent<T> | Mouse button is released on element |
onMouseEnter | React.MouseEvent<T> | Pointer enters the element (does NOT bubble) |
onMouseLeave | React.MouseEvent<T> | Pointer leaves the element (does NOT bubble) |
onMouseOver | React.MouseEvent<T> | Pointer enters the element or a child (bubbles) |
onMouseOut | React.MouseEvent<T> | Pointer leaves the element or enters a child (bubbles) |
onMouseMove | React.MouseEvent<T> | Pointer moves while over the element |
onContextMenu | React.MouseEvent<T> | Right-click or context menu key is pressed |
Quick-reference recipe card -- copy-paste ready.
// Click handler with typed event
function ClickExample() {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("Clicked at", e.clientX, e.clientY);
};
return <button onClick={handleClick}>Click me</button>;
}
// Hover with onMouseEnter / onMouseLeave
function HoverExample() {
const [hovered, setHovered] = React.useState(false);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ background: hovered ? "#e0e7ff" : "#fff" }}
>
Hover me
</div>
);
}
// Right-click context menu
function ContextMenuExample() {
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
console.log("Custom menu at", e.clientX, e.clientY);
};
return <div onContextMenu={handleContextMenu}>Right-click me</div>;
}
// Double-click
function DoubleClickExample() {
const handleDoubleClick = (e: React.MouseEvent<HTMLSpanElement>) => {
console.log("Double-clicked");
};
return <span onDoubleClick={handleDoubleClick}>Double-click to edit</span>;
}When to reach for this: You need to respond to mouse interactions -- clicks, hovers, right-clicks, or position tracking -- in a client component.
"use client";
import { useState, useRef, useCallback } from "react";
type ContextMenuState = {
visible: boolean;
x: number;
y: number;
};
export default function InteractiveCard() {
const [hovered, setHovered] = useState(false);
const [clickCount, setClickCount] = useState(0);
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
visible: false,
x: 0,
y: 0,
});
const cardRef = useRef<HTMLDivElement>(null);
const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
// Close context menu on regular click
if (contextMenu.visible) {
setContextMenu((prev) => ({ ...prev, visible: false }));
return;
}
setClickCount((c) => c + 1);
},
[contextMenu.visible]
);
const handleContextMenu = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
const rect = cardRef.current?.getBoundingClientRect();
if (!rect) return;
setContextMenu({
visible: true,
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
},
[]
);
const handleMenuAction = useCallback((action: string) => {
console.log("Menu action:", action);
setContextMenu((prev) => ({ ...prev, visible: false }));
}, []);
return (
<div
ref={cardRef}
onClick={handleClick}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => {
setHovered(false);
setContextMenu((prev) => ({ ...prev, visible: false }));
}}
onContextMenu={handleContextMenu}
style={{
position: "relative",
padding: "24px",
border: `2px solid ${hovered ? "#6366f1" : "#e5e7eb"}`,
borderRadius: "12px",
background: hovered ? "#f5f3ff" : "#fff",
transition: "all 150ms ease",
cursor: "pointer",
userSelect: "none",
}}
>
<h3>Interactive Card</h3>
<p>Clicked {clickCount} times. Right-click for context menu.</p>
{contextMenu.visible && (
<ul
style={{
position: "absolute",
top: contextMenu.y,
left: contextMenu.x,
background: "#fff",
border: "1px solid #e5e7eb",
borderRadius: "8px",
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
listStyle: "none",
padding: "4px 0",
margin: 0,
zIndex: 10,
}}
>
{["Edit", "Duplicate", "Delete"].map((action) => (
<li
key={action}
onClick={(e) => {
e.stopPropagation();
handleMenuAction(action);
}}
onMouseEnter={(e) =>
(e.currentTarget.style.background = "#f3f4f6")
}
onMouseLeave={(e) =>
(e.currentTarget.style.background = "transparent")
}
style={{ padding: "8px 16px", cursor: "pointer" }}
>
{action}
</li>
))}
</ul>
)}
</div>
);
}What this demonstrates:
onClick, onMouseEnter, onMouseLeave, and onContextMenu on a single elementgetBoundingClientRecte.stopPropagation() to prevent menu item clicks from triggering the card click handlere.preventDefault() on onContextMenu to suppress the browser default menuReact.MouseEvent<T> that wraps the native MouseEvent with a consistent cross-browser interface.onMouseEnter and onMouseLeave do NOT bubble. They fire only for the exact element, not its children. Use onMouseOver and onMouseOut if you need bubbling behavior.setTimeout), read the values you need into local variables first.Click with data passing:
type Item = { id: string; name: string };
function ItemList({ items }: { items: Item[] }) {
const handleClick = (item: Item) => (e: React.MouseEvent<HTMLLIElement>) => {
console.log("Selected:", item.id, "at", e.clientX);
};
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={handleClick(item)}>
{item.name}
</li>
))}
</ul>
);
}Hover tracking with coordinates:
function HoverTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
setPosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
},
[]
);
return (
<div onMouseMove={handleMouseMove} style={{ width: 300, height: 300 }}>
Cursor at ({position.x}, {position.y})
</div>
);
}Drag-start detection (mousedown + mousemove threshold):
function DragDetector() {
const startPos = useRef<{ x: number; y: number } | null>(null);
const [dragging, setDragging] = useState(false);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
startPos.current = { x: e.clientX, y: e.clientY };
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!startPos.current) return;
const dx = e.clientX - startPos.current.x;
const dy = e.clientY - startPos.current.y;
if (Math.sqrt(dx * dx + dy * dy) > 5) {
setDragging(true);
}
};
const handleMouseUp = () => {
startPos.current = null;
setDragging(false);
};
return (
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
{dragging ? "Dragging..." : "Click and drag"}
</div>
);
}Long press detection:
function LongPressButton({ onLongPress }: { onLongPress: () => void }) {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseDown = () => {
timerRef.current = setTimeout(onLongPress, 600);
};
const handleMouseUp = () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
return (
<button onMouseDown={handleMouseDown} onMouseUp={handleMouseUp}>
Long press me
</button>
);
}Right-click context menu with portal:
import { createPortal } from "react-dom";
function ContextMenuPortal({
x,
y,
onClose,
}: {
x: number;
y: number;
onClose: () => void;
}) {
return createPortal(
<div
style={{ position: "fixed", top: y, left: x, zIndex: 9999 }}
onClick={onClose}
>
<ul style={{ background: "#fff", border: "1px solid #ddd", padding: 8 }}>
<li>Copy</li>
<li>Paste</li>
</ul>
</div>,
document.body
);
}// The generic T specifies the element type for e.currentTarget
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget; // HTMLButtonElement -- the element the handler is attached to
e.target; // EventTarget -- the actual element clicked (could be a child)
};
// Use HTMLElement as a generic fallback when the exact element varies
const handleAny = (e: React.MouseEvent<HTMLElement>) => {
e.currentTarget.dataset.id; // works for any HTML element
};
// Narrowing e.target (it is typed as EventTarget, not Element)
const handleDelegated = (e: React.MouseEvent<HTMLUListElement>) => {
const target = e.target as HTMLElement;
if (target.tagName === "LI") {
console.log(target.textContent);
}
};
// Mouse button detection
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
// e.button: 0 = left, 1 = middle, 2 = right
if (e.button === 2) console.log("Right click");
};
// Coordinates available on every mouse event
// e.clientX / e.clientY -- relative to viewport
// e.pageX / e.pageY -- relative to document
// e.screenX / e.screenY -- relative to monitor
// e.nativeEvent.offsetX / offsetY -- relative to target elementonMouseEnter/onMouseLeave vs onMouseOver/onMouseOut -- onMouseEnter and onMouseLeave do NOT bubble and do NOT fire when moving between child elements. onMouseOver and onMouseOut DO bubble, so they fire when entering/leaving children too. Use the wrong pair and hover states will flicker on nested elements.
Creating new arrow functions in JSX on every render -- Writing onClick={() => handleClick(id)} creates a new function reference each render, which can cause unnecessary re-renders in child components that rely on referential equality. Fix: Use useCallback or extract the handler to a stable reference. For lists, consider a child component that receives the item and attaches its own handler.
onClick fires on ANY mouse button by default -- React's onClick fires for left-clicks only in most browsers, but this is not guaranteed for all elements and browsers. If you need to distinguish buttons, check e.button in onMouseDown or onMouseUp.
e.target vs e.currentTarget confusion -- e.target is the element that was actually clicked (could be a child). e.currentTarget is the element the handler is attached to. If you click a <span> inside a <button>, e.target is the span, e.currentTarget is the button. Accessing properties like .value or .dataset on e.target requires type narrowing.
onDoubleClick fires two onClick events first -- A double-click triggers: onClick -> onClick -> onDoubleClick. If your onClick handler performs an action, you will get that action twice before the double-click fires. Fix: Use a debounce timer to distinguish single from double clicks, or avoid combining both on the same element.
Synthetic events are nullified after the handler -- Accessing e.clientX inside a setTimeout or after an await returns null because React recycles the event object. Fix: Read values into local variables before any async operation: const x = e.clientX;.
onMouseMove fires very frequently -- Attaching state updates to onMouseMove can cause hundreds of re-renders per second. Fix: Throttle with requestAnimationFrame or a throttle utility, or use a ref to store position and only re-render when needed.
| Alternative | Use When | Don't Use When |
|---|---|---|
Pointer Events (onPointerDown, etc.) | You need to support touch, pen, and mouse uniformly | You only target mouse on desktop |
CSS :hover | Hover styles only, no JS logic needed | You need to track hover state or coordinates in JS |
Drag and Drop API (onDragStart, etc.) | You need full drag-and-drop with data transfer | You just need click-and-drag movement detection |
addEventListener in useEffect | You need global listeners (e.g., document-level mouse tracking) | The event is scoped to a specific element |
| Third-party libraries (dnd-kit, Framer Motion) | Complex drag, gestures, or animation tied to mouse position | Simple click or hover handlers |
onMouseEnter / onMouseLeave do NOT bubble and fire only for the exact element, not its childrenonMouseOver / onMouseOut DO bubble, so they fire when entering/leaving child elements tooEnter/Leave for hover state on a single element; use Over/Out when you need bubblinge.target is the element that was actually clicked (could be a nested child)e.currentTarget is the element the handler is attached to<span> inside a <button>, e.target is the span, e.currentTarget is the buttonUse e.button in onMouseDown or onMouseUp:
0 = left button1 = middle button2 = right buttonDo not rely on onClick for button detection -- it is primarily for left-clicks.
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault(); // suppress browser default menu
setMenu({ visible: true, x: e.clientX, y: e.clientY });
};
return <div onContextMenu={handleContextMenu}>Right-click me</div>;A double-click triggers: onClick -> onClick -> onDoubleClick. If your onClick performs an action, it runs twice before the double-click fires. Use a debounce timer to distinguish single from double clicks, or avoid combining both handlers on the same element.
React's synthetic events are nullified after the handler returns. Accessing properties later gives null. Read values into local variables before any async operation: const x = e.clientX;.
function ItemList({ items }: { items: Item[] }) {
const handleClick = (item: Item) =>
(e: React.MouseEvent<HTMLLIElement>) => {
console.log("Selected:", item.id);
};
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={handleClick(item)}>{item.name}</li>
))}
</ul>
);
}For better performance, extract each item into a child component with its own stable handler.
onMouseMove fires very frequently (hundreds of times per second). Attaching state updates to it causes excessive re-renders. Throttle with requestAnimationFrame, use a throttle utility, or store position in a ref and only re-render when needed.
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseDown = () => {
timerRef.current = setTimeout(onLongPress, 600);
};
const handleMouseUp = () => {
if (timerRef.current) clearTimeout(timerRef.current);
};clientX / clientY -- relative to the viewportpageX / pageY -- relative to the document (includes scroll offset)screenX / screenY -- relative to the monitornativeEvent.offsetX / offsetY -- relative to the target element// Specific element type
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget; // HTMLButtonElement
};
// Generic fallback for any HTML element
const handleAny = (e: React.MouseEvent<HTMLElement>) => {
e.currentTarget.dataset.id; // works for any element
};const handleDelegated = (e: React.MouseEvent<HTMLUListElement>) => {
// e.target is typed as EventTarget, not Element
const target = e.target as HTMLElement;
if (target.tagName === "LI") {
console.log(target.textContent);
}
};Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥