@visx/event
@visx/event is a tiny browser utility for turning mouse, pointer, touch, or focus events into x and y coordinates usable by an SVG visualization. Its main localPoint function accepts an event alone or an explicit DOM element plus an event, accounts for an SVG screen transformation when available, and returns a @visx/point Point. The package also exports touchPoint as the explicit node-and-event form. It does not manage tooltips, gestures, selections, scales, or chart state.
A sharp, minimal choice for SVG pointer coordinates inside a visx 4 chart. Skip it for HTML-only positioning or real gesture handling, and be deliberate about viewBox, nested-group, and portal coordinate spaces.
Use it if
- You build custom SVG charts with visx and need pointer coordinates in the outer SVG's coordinate system
- You need one helper that accepts native DOM events and React synthetic mouse, touch, or focus events
- You want transformed SVG coordinates from getScreenCTM rather than raw page or viewport pixels
- You already use other visx 4 packages and want a small tree-shakeable coordinate primitive with matching types
- You only need coordinates relative to an ordinary HTML element: getBoundingClientRect plus event.clientX and clientY avoids React and @visx/point dependencies
- Your app is on React 16 or 17: visx 4 requires React 18 or 19, and the migration guide says older React apps should stay on visx 3
- You need drag, pinch, velocity, capture, multi-touch, or keyboard interaction state: localPoint converts one event and touch handling reads only the first changed touch
- You need coordinates in a transformed nested g element rather than the top-level owner SVG: localPoint deliberately finds the top-most SVG, so you must invert the group's own screen CTM yourself
- You render Canvas, WebGL, or a portal whose coordinate space differs from the SVG: this helper returns SVG or element-local coordinates and does not translate them into page, canvas backing-store, or portal coordinates
Setup reality
Install `@visx/event@^4` with React 18 or 19; React is a peer dependency even though this package exports functions rather than components. TypeScript declarations ship with the package, but `@types/react` is an optional peer and should match your React major in TypeScript projects. The sole runtime dependency is the exactly aligned `@visx/point` package. The visx 4 migration guide recommends upgrading every `@visx/*` package together because entry points, peer ranges, and internal versions changed as a set. Import only from `@visx/event`; v4's exports map blocks unsupported deep imports. There is no provider, CSS file, credential, native build, or configuration step. The subtle work is coordinate space. With an event from an SVG child, localPoint finds the top-level owner SVG and applies the inverse screen CTM, which handles a viewBox and CSS scaling. It does not return coordinates local to a transformed group. When the SVG transform path is unavailable, including common explicit-root cases, it falls back to client coordinates minus the element's bounding rectangle and client border. That fallback is CSS-pixel math and may not match viewBox units. The one-argument form depends on event.target; the two-argument form lets you pin the reference element when delegation or overlays make the target unstable. Always handle a null result. Touch uses changedTouches[0], so it is not a gesture engine, and preventing browser scroll during drawing usually also needs `touch-action: none`. Focus events have no pointer position, so the implementation uses the center of the focused target's bounding box. Finally, do not pass local SVG coordinates straight to a body-level tooltip portal without converting coordinate systems or using the tooltip package's container strategy.
Patterns
Read mouse coordinates inside an SVGtrack-mouse-position
import { localPoint } from '@visx/event';
function Overlay() {
return (
<rect
width={600}
height={300}
fill="transparent"
onMouseMove={(event) => {
const point = localPoint(event);
if (point) console.log(point.x, point.y);
}}
/>
);
}The event-only form uses event.target and, for an SVG child, transforms viewport coordinates into the top-level owner's SVG coordinate system.
Anchor coordinates to an explicit SVGuse-explicit-svg-ref
import { useRef } from 'react';
import { localPoint } from '@visx/event';
function Chart() {
const svgRef = useRef<SVGSVGElement>(null);
return (
<svg ref={svgRef} onMouseMove={(event) => {
const svg = svgRef.current;
if (!svg) return;
const point = localPoint(svg, event);
if (point) console.log(point);
}}>
{/* marks */}
</svg>
);
}An explicit node avoids unstable event targets. For a scaled viewBox, verify the result because a root SVG can take the bounding-box fallback path.
Use one handler for mouse, pen, and touch pointershandle-pointer-events
function Mark() {
return (
<circle
cx={80}
cy={60}
r={12}
onPointerMove={(event) => {
const point = localPoint(event);
if (point) updateCursor(point);
}}
/>
);
}Pointer events expose clientX and clientY and follow the mouse-event branch at runtime. Gesture state and pointer capture remain your responsibility.
Read the first changed touchhandle-touch-event
<svg
style={{ touchAction: 'none' }}
onTouchMove={(event) => {
const point = localPoint(event);
if (point) drawAt(point.x, point.y);
}}
/>;The implementation uses changedTouches[0]. touch-action: none prevents browser panning for a drawing surface, but this is not multi-touch tracking.
Call the explicit touchPoint exportuse-touch-point
import { touchPoint } from '@visx/event';
function onTouchMove(event: React.TouchEvent<SVGSVGElement>) {
const svg = event.currentTarget;
const point = touchPoint(svg, event);
if (point) console.log(point.x, point.y);
}touchPoint is the generic node-plus-event converter despite its name; localPoint is usually clearer because it supports both public signatures.
Position a tooltip from focusposition-keyboard-tooltip
<circle
tabIndex={0}
cx={x}
cy={y}
r={6}
onFocus={(event) => {
const point = localPoint(event);
if (point) showTooltip({ left: point.x, top: point.y });
}}
/>;Focus events have no pointer coordinates, so localPoint uses the visual center of the focused target's bounding rectangle.
Convert pointer position back through a scalefind-nearest-datum
function onMouseMove(event: React.MouseEvent<SVGRectElement>) {
const point = localPoint(event);
if (!point) return;
const domainX = xScale.invert(point.x);
const nearest = findNearestDatum(data, domainX);
setHovered(nearest);
}invert is available on continuous scales, not band scales. For bands, compare point.x with band positions and bandwidth instead.
Use event-target conversion with a scaled viewBoxrespect-svg-viewbox
<svg viewBox="0 0 1000 500" width="500" height="250">
<rect
width={1000}
height={500}
fill="transparent"
onMouseMove={(event) => {
const point = localPoint(event);
if (point) setCrosshair(point);
}}
/>
</svg>;Because the target is an SVG child, localPoint can invert the owner SVG's screen CTM and return viewBox user units instead of CSS pixels.
Convert into a transformed group's local spaceconvert-group-coordinates
function pointInGroup(group: SVGGElement, event: React.MouseEvent) {
const ctm = group.getScreenCTM();
if (!ctm) return null;
const point = group.ownerSVGElement!.createSVGPoint();
point.x = event.clientX;
point.y = event.clientY;
return point.matrixTransform(ctm.inverse());
}localPoint targets the top-level owner SVG. Invert the group's own CTM when zoom, rotation, or nested transforms require group-local coordinates.
Guard a missing coordinate conversionhandle-null-result
const point = localPoint(event);
if (!point) {
hideTooltip();
return;
}
showTooltip({ left: point.x, top: point.y });Do not silently replace null with {x: 0, y: 0}; that produces a visible jump to the chart origin when no valid target is available.
Place a tooltip inside the same chart containerplace-overlay-tooltip
const point = localPoint(event);
if (point) {
setTooltip({
datum,
left: point.x + margin.left,
top: point.y + margin.top,
});
}This works when the overlay shares the chart container's coordinate space. A body-level portal needs page or container conversion, not raw SVG-local values.
Coalesce high-frequency pointer updatesthrottle-pointer-updates
let frame = 0;
function onPointerMove(event: React.PointerEvent<SVGRectElement>) {
const point = localPoint(event);
if (!point) return;
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => setCursor(point));
}localPoint is small, but React state updates and nearest-datum searches can dominate pointer-move cost. Cancel the pending frame during unmount in production code.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| d3-selection | npm | You already use D3 and want its pointer or pointers helpers, including multiple touch points |
| @use-gesture/react | npm | You need stateful drag, pinch, wheel, hover, or move gestures rather than one coordinate conversion |
| @visx/drag | npm | You want visx drag components or hooks with start, move, end, delta, and reset state |
| @visx/tooltip | npm | Your actual task is managing and rendering chart tooltips, including portal-aware placement |