Mouse Events
Handle clicks, double-clicks, hovers, and mouse movement in React components.
Mouse Event Reference
| 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 |
Recipe
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.
Working Example
"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:
- Combining
onClick,onMouseEnter,onMouseLeave, andonContextMenuon a single element - Positioning a custom context menu relative to the card using
getBoundingClientRect - Using
e.stopPropagation()to prevent menu item clicks from triggering the card click handler - Using
e.preventDefault()ononContextMenuto suppress the browser default menu - TypeScript typing for state, refs, and event handlers
Deep Dive
How It Works
- React normalizes mouse events through its Synthetic Event system. Every mouse event handler receives a
React.MouseEvent<T>that wraps the nativeMouseEventwith a consistent cross-browser interface. - React uses event delegation -- a single listener is attached at the root, not on each element. Your handler is invoked during the bubble phase by default.
onMouseEnterandonMouseLeavedo NOT bubble. They fire only for the exact element, not its children. UseonMouseOverandonMouseOutif you need bubbling behavior.- The Synthetic Event is pooled and reused. If you need to access the event asynchronously (e.g., in a
setTimeout), read the values you need into local variables first.
Variations
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
);
}TypeScript Notes
// 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 elementGotchas
-
onMouseEnter/onMouseLeavevsonMouseOver/onMouseOut--onMouseEnterandonMouseLeavedo NOT bubble and do NOT fire when moving between child elements.onMouseOverandonMouseOutDO 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: UseuseCallbackor extract the handler to a stable reference. For lists, consider a child component that receives the item and attaches its own handler. -
onClickfires on ANY mouse button by default -- React'sonClickfires for left-clicks only in most browsers, but this is not guaranteed for all elements and browsers. If you need to distinguish buttons, checke.buttoninonMouseDownoronMouseUp. -
e.targetvse.currentTargetconfusion --e.targetis the element that was actually clicked (could be a child).e.currentTargetis the element the handler is attached to. If you click a<span>inside a<button>,e.targetis the span,e.currentTargetis the button. Accessing properties like.valueor.datasetone.targetrequires type narrowing. -
onDoubleClickfires twoonClickevents first -- A double-click triggers:onClick->onClick->onDoubleClick. If youronClickhandler 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.clientXinside asetTimeoutor after anawaitreturnsnullbecause React recycles the event object. Fix: Read values into local variables before any async operation:const x = e.clientX;. -
onMouseMovefires very frequently -- Attaching state updates toonMouseMovecan cause hundreds of re-renders per second. Fix: Throttle withrequestAnimationFrameor a throttle utility, or use a ref to store position and only re-render when needed.
Alternatives
| 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 |
FAQs
What is the difference between onMouseEnter/onMouseLeave and onMouseOver/onMouseOut?
onMouseEnter/onMouseLeavedo NOT bubble and fire only for the exact element, not its childrenonMouseOver/onMouseOutDO bubble, so they fire when entering/leaving child elements too- Use
Enter/Leavefor hover state on a single element; useOver/Outwhen you need bubbling
What is the difference between e.target and e.currentTarget?
e.targetis the element that was actually clicked (could be a nested child)e.currentTargetis the element the handler is attached to- If you click a
<span>inside a<button>,e.targetis the span,e.currentTargetis the button
How do you detect which mouse button was pressed?
Use e.button in onMouseDown or onMouseUp:
0= left button1= middle button2= right button
Do not rely on onClick for button detection -- it is primarily for left-clicks.
How do you implement a custom right-click context menu?
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>;Gotcha: Why does onDoubleClick also fire onClick twice?
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.
Why can't you access e.clientX inside a setTimeout or after an await?
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;.
How do you pass data to a click handler in a list without creating a new function on every render?
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.
Gotcha: Why does onMouseMove cause performance issues?
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.
How do you implement long press detection with mouse events?
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseDown = () => {
timerRef.current = setTimeout(onLongPress, 600);
};
const handleMouseUp = () => {
if (timerRef.current) clearTimeout(timerRef.current);
};What coordinate properties are available on mouse events, and how do they differ?
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
What is the correct TypeScript generic type for mouse event handlers?
// 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
};How do you narrow the type of e.target when using event delegation in TypeScript?
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);
}
};Related
- Keyboard Events -- Responding to key presses and keyboard shortcuts
- Form Events -- Handling form submissions and input changes