Eventos de Ponteiro
Manipulação unificada de entrada que funciona em mouse, toque e caneta -- a substituição moderna para eventos de mouse e toque separados.
Busque em todas as páginas da documentação
Manipulação unificada de entrada que funciona em mouse, toque e caneta -- a substituição moderna para eventos de mouse e toque separados.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Evento | Dispara Quando | Objeto de Evento |
|---|---|---|
onPointerDown | Um ponteiro se torna ativo (botão pressionado, toque, contato da caneta) | React.PointerEvent |
onPointerUp | Um ponteiro é liberado | React.PointerEvent |
onPointerMove | Um ponteiro muda de posição | React.PointerEvent |
onPointerEnter | Um ponteiro entra nos limites do elemento (não borbulha) | React.PointerEvent |
onPointerLeave | Um ponteiro sai dos limites do elemento (não borbulha) | React.PointerEvent |
onPointerOver | Um ponteiro entra nos limites do elemento (borbulha) | React.PointerEvent |
onPointerOut | Um ponteiro sai dos limites do elemento (borbulha) | React.PointerEvent |
onPointerCancel | O sistema cancela a interação do ponteiro | React.PointerEvent |
onGotPointerCapture | O elemento recebe captura de ponteiro | React.PointerEvent |
onLostPointerCapture | O elemento perde a captura de ponteiro | React.PointerEvent |
Propriedades chave em React.PointerEvent:
| Propriedade | Tipo | Descrição |
|---|---|---|
pointerId | number | Identificador único para o ponteiro |
pointerType | "mouse" | "touch" | "pen" | Tipo de dispositivo de entrada |
pressure | number | Pressão de 0 a 1 (0.5 para botões do mouse sem sensor de pressão) |
width | number | Largura do contato em pixels CSS |
height | number | Altura do contato em pixels CSS |
tiltX | number | Inclinação da caneta no eixo X (de -90 a 90 graus) |
tiltY | number | Inclinação da caneta no eixo Y (de -90 a 90 graus) |
isPrimary | boolean | Se este é o ponteiro primário do seu tipo |
Cartão de receita de referência rápida -- pronto para copiar e colar.
// Estado de pressionamento com eventos de ponteiro
"use client";
import { useState, useCallback } from "react";
export function PressableButton({ children }: { children: React.ReactNode }) {
const [pressed, setPressed] = useState(false);
return (
<button
className={`px-6 py-3 rounded-lg transition-transform ${
pressed ? "scale-95 bg-blue-700" : "scale-100 bg-blue-600"
} text-white`}
onPointerDown={() => setPressed(true)}
onPointerUp={() => setPressed(false)}
onPointerLeave={() => setPressed(false)}
>
{children}
</button>
);
}// Rastreamento de posição do ponteiro
"use client";
import { useState, useCallback } from "react";
export function PointerTracker() {
const [pos, setPos] = useState({ x: 0, y: 0 });
const handleMove = useCallback((e: React.PointerEvent) => {
setPos({ x: e.clientX, y: e.clientY });
}, []);
return (
<div className="h-64 bg-gray-100 relative" onPointerMove={handleMove}>
<div
className="w-4 h-4 bg-red-500 rounded-full absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none"
style={{ left: pos.x, top: pos.y }}
/>
</div>
);
}Quando usar isso: Você deseja manipulação de entrada que funcione identicamente para mouse, toque e caneta, sem escrever manipuladores separados para cada um.
// DrawingCanvas.tsx
"use client";
import { useRef, useState, useCallback } from "react";
type Point = { x: number; y: number; pressure: number };
type Stroke = Point[];
export default function DrawingCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [strokes, setStrokes] = useState<Stroke[]>([]);
const currentStroke = useRef<Stroke>([]);
const getCanvasPoint = useCallback(
(e: React.PointerEvent<HTMLCanvasElement>): Point => {
const rect = e.currentTarget.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
pressure: e.pressure,
};
},
[]
);
const drawStroke = useCallback(
(ctx: CanvasRenderingContext2D, points: Point[]) => {
if (points.length < 2) return;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
const p = points[i];
ctx.lineWidth = 2 + p.pressure * 6; // largura sensível à pressão
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
},
[]
);
const redrawAll = useCallback(
(allStrokes: Stroke[]) => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = "#1e293b";
ctx.lineCap = "round";
ctx.lineJoin = "round";
allStrokes.forEach((stroke) => drawStroke(ctx, stroke));
},
[drawStroke]
);
const handlePointerDown = useCallback(
(e: React.PointerEvent<HTMLCanvasElement>) => {
e.currentTarget.setPointerCapture(e.pointerId);
setIsDrawing(true);
currentStroke.current = [getCanvasPoint(e)];
},
[getCanvasPoint]
);
const handlePointerMove = useCallback(
(e: React.PointerEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
const point = getCanvasPoint(e);
currentStroke.current.push(point);
// Desenha incrementalmente para responsividade
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (!ctx) return;
const points = currentStroke.current;
if (points.length < 2) return;
ctx.strokeStyle = "#1e293b";
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = 2 + point.pressure * 6;
ctx.beginPath();
ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y);
ctx.lineTo(point.x, point.y);
ctx.stroke();
},
[isDrawing, getCanvasPoint]
);
const handlePointerUp = useCallback(() => {
if (!isDrawing) return;
setIsDrawing(false);
const finished = [...currentStroke.current];
setStrokes((prev) => [...prev, finished]);
currentStroke.current = [];
}, [isDrawing]);
const handleClear = useCallback(() => {
setStrokes([]);
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (ctx && canvas) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}, []);
const handleUndo = useCallback(() => {
setStrokes((prev) => {
const next = prev.slice(0, -1);
redrawAll(next);
return next;
});
}, [redrawAll]);
return (
<div className="space-y-4">
<div className="flex gap-2">
<button
onClick={handleUndo}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
Desfazer
</button>
<button
onClick={handleClear}
className="px-4 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200"
>
Limpar
</button>
<span className="self-center text-sm text-gray-500">
{strokes.length} traço{strokes.length !== 1 ? "s" : ""}
</span>
</div>
<canvas
ref={canvasRef}
width={600}
height={400}
className="border border-gray-300 rounded-lg cursor-crosshair touch-none"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
/>
</div>
);
}O que isso demonstra:
setPointerCapture para continuar recebendo eventos mesmo quando o ponteiro sai da telatouch-none CSS para evitar a rolagem do navegador durante o desenhopointerId único que persiste durante toda a vida útil dessa interação.pointerType informa o dispositivo de entrada para que você possa adaptar o comportamento (por exemplo, mostrar pré-visualização de hover para mouse, pular para toque).setPointerCapture(pointerId) redireciona todos os eventos para aquele ponteiro para o elemento de captura, mesmo que o ponteiro se mova para fora dele. Isso é essencial para operações de arrastar.preventDefault() em um evento de ponteiro, o evento de mouse será suprimido.Desenho sensível à pressão com entrada de caneta:
"use client";
import { useCallback } from "react";
export function PressureLine() {
const handleMove = useCallback(
(e: React.PointerEvent<HTMLCanvasElement>) => {
const canvas = e.currentTarget;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// pressure: 0 = hover, 0-1 = pressão de contato
// Para mouses sem pressão, a pressão é 0.5 quando um botão é pressionado
const lineWidth = e.pointerType === "pen" ? e.pressure * 10 : 2;
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.lineTo(
e.clientX - canvas.getBoundingClientRect().left,
e.clientY - canvas.getBoundingClientRect().top
);
ctx.stroke();
},
[]
);
return (
<canvas
width={600}
height={400}
className="border touch-none"
onPointerMove={handleMove}
/>
);
}Captura de ponteiro para arrastar confiável:
"use client";
import { useState, useRef, useCallback } from "react";
export function DraggableBox() {
const [position, setPosition] = useState({ x: 100, y: 100 });
const dragOffset = useRef({ x: 0, y: 0 });
const handlePointerDown = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
e.currentTarget.setPointerCapture(e.pointerId);
dragOffset.current = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
},
[position]
);
const handlePointerMove = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
setPosition({
x: e.clientX - dragOffset.current.x,
y: e.clientY - dragOffset.current.y,
});
},
[]
);
const handlePointerUp = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
e.currentTarget.releasePointerCapture(e.pointerId);
},
[]
);
return (
<div
className="w-24 h-24 bg-purple-500 rounded-lg cursor-grab active:cursor-grabbing absolute"
style={{ left: position.x, top: position.y }}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
/>
);
}Detecção de hover entre tipos de dispositivo:
"use client";
import { useState, useCallback } from "react";
export function AdaptiveHover({ children }: { children: React.ReactNode }) {
const [hovered, setHovered] = useState(false);
const [inputType, setInputType] = useState<string>("mouse");
const handleEnter = useCallback((e: React.PointerEvent) => {
setInputType(e.pointerType);
// Mostra o estado de hover apenas para mouse/caneta -- toque não tem hover
if (e.pointerType !== "touch") {
setHovered(true);
}
}, []);
const handleLeave = useCallback(() => {
setHovered(false);
}, []);
return (
<div
className={`p-4 rounded-lg border-2 transition-colors ${
hovered ? "border-blue-500 bg-blue-50" : "border-gray-200"
}`}
onPointerEnter={handleEnter}
onPointerLeave={handleLeave}
>
{children}
<p className="text-xs text-gray-400 mt-2">Entrada: {inputType}</p>
</div>
);
}Rastreamento de múltiplos ponteiros:
"use client";
import { useState, useCallback } from "react";
type PointerInfo = {
id: number;
x: number;
y: number;
type: string;
};
export function MultiPointerTracker() {
const [pointers, setPointers] = useState<Map<number, PointerInfo>>(new Map());
const handlePointerDown = useCallback((e: React.PointerEvent) => {
setPointers((prev) => {
const next = new Map(prev);
next.set(e.pointerId, {
id: e.pointerId,
x: e.clientX,
y: e.clientY,
type: e.pointerType,
});
return next;
});
}, []);
const handlePointerMove = useCallback((e: React.PointerEvent) => {
setPointers((prev) => {
if (!prev.has(e.pointerId)) return prev;
const next = new Map(prev);
next.set(e.pointerId, {
id: e.pointerId,
x: e.clientX,
y: e.clientY,
type: e.pointerType,
});
return next;
});
}, []);
const handlePointerUp = useCallback((e: React.PointerEvent) => {
setPointers((prev) => {
const next = new Map(prev);
next.delete(e.pointerId);
return next;
});
}, []);
return (
<div
className="h-96 bg-gray-50 relative touch-none"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
>
{Array.from(pointers.values()).map((p) => (
<div
key={p.id}
className="absolute w-8 h-8 rounded-full -translate-x-1/2 -translate-y-1/2 flex items-center justify-center text-white text-xs pointer-events-none"
style={{
left: p.x,
top: p.y,
backgroundColor: p.type === "touch" ? "#ef4444" : "#3b82f6",
}}
>
{p.id}
</div>
))}
<p className="absolute bottom-4 left-4 text-sm text-gray-500">
Ponteiros ativos: {pointers.size}
</p>
</div>
);
}Eventos de ponteiro vs. eventos de mouse -- comparação:
// Eventos de mouse -- manipuladores separados necessários para toque vs. mouse
<div
onMouseDown={handleMouseDown} // apenas mouse
onMouseMove={handleMouseMove} // apenas mouse
onMouseUp={handleMouseUp} // apenas mouse
onTouchStart={handleTouchStart} // apenas toque
onTouchMove={handleTouchMove} // apenas toque
onTouchEnd={handleTouchEnd} // apenas toque
/>
// Eventos de ponteiro -- conjunto único de manipuladores para todos os tipos de entrada
<div
onPointerDown={handlePointerDown} // mouse + toque + caneta
onPointerMove={handlePointerMove} // mouse + toque + caneta
onPointerUp={handlePointerUp} // mouse + toque + caneta
/>// O tipo de evento inclui propriedades específicas do ponteiro
function handlePointer(e: React.PointerEvent<HTMLDivElement>) {
const id: number = e.pointerId;
const type: string = e.pointerType; // "mouse" | "touch" | "pen"
const pressure: number = e.pressure; // 0 a 1
const isPrimary: boolean = e.isPrimary;
// Herdado de MouseEvent
const x: number = e.clientX;
const y: number = e.clientY;
const button: number = e.button;
}
// Delimitando por pointerType
function handleAdaptive(e: React.PointerEvent) {
switch (e.pointerType) {
case "mouse":
// Tem hover, clique com o botão direito
break;
case "touch":
// Sem hover, tem largura/altura para área de contato
break;
case "pen":
// Tem pressão, inclinação
const tiltX: number = e.tiltX;
const tiltY: number = e.tiltY;
break;
}
}
// Métodos de captura de ponteiro no elemento
function handleDown(e: React.PointerEvent<HTMLDivElement>) {
const el: HTMLDivElement = e.currentTarget;
el.setPointerCapture(e.pointerId); // retorna void
el.hasPointerCapture(e.pointerId); // retorna boolean
el.releasePointerCapture(e.pointerId); // retorna void
}
// Map para rastrear múltiplos ponteiros
const pointers = useRef<Map<number, { x: number; y: number }>>(new Map());Eventos de ponteiro não fornecem touches / changedTouches -- Ao contrário dos eventos de toque, os eventos de ponteiro disparam uma vez por ponteiro, não uma vez com uma lista de todos os toques. Correção: Se você precisar de detalhes de multi-toque, como distância de pinça, use eventos de toque ou rastreie cada pointerId em um Map.
setPointerCapture deve ser chamado dentro de onPointerDown -- Chamá-lo em outros manipuladores ou após uma operação assíncrona falha silenciosamente ou lança um erro. Correção: Sempre chame e.currentTarget.setPointerCapture(e.pointerId) de forma síncrona dentro de onPointerDown.
Eventos de mouse ainda disparam após eventos de ponteiro -- Eventos de ponteiro não suprimem automaticamente eventos de mouse. Tanto onPointerDown quanto onMouseDown serão disparados para entrada de mouse. Correção: Remova os manipuladores de eventos de mouse ao usar eventos de ponteiro, ou chame e.preventDefault() no manipulador de ponteiro para suprimir o evento de mouse.
pressure é sempre 0.5 para botões do mouse -- Mouses sem sensores de pressão relatam 0.5 quando qualquer botão é pressionado e 0 caso contrário. Correção: Verifique e.pointerType === "pen" antes de usar pressure para recursos sensíveis à pressão.
A rolagem de toque não é impedida apenas por manipuladores de ponteiro -- Manipular onPointerMove não impede que o navegador role em toques. Correção: Adicione touch-action: none como propriedade CSS ao elemento, ou use e.preventDefault() no pointermove nativo via addEventListener com { passive: false }.
onPointerLeave dispara durante a captura de ponteiro -- Quando você tem captura de ponteiro e o ponteiro se move para fora do elemento, pointerleave ainda dispara, mesmo que você ainda esteja recebendo pointermove. Correção: Use onGotPointerCapture / onLostPointerCapture para rastrear o estado de captura e ignorar eventos de saída enquanto capturado.
pointerId muda entre sequências de toque -- Levantar e recolocar um dedo dá a ele um novo pointerId. Não assuma que o mesmo dedo terá o mesmo ID entre sequências de toque separadas. Correção: Limpe seu estado de rastreamento de ponteiro em pointerup / pointercancel.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
Eventos de Mouse (onMouseDown, etc.) | Você só suporta desktop e precisa de tratamento de menu de contexto com clique direito | Você precisa de suporte a toque/caneta |
Eventos de Toque (onTouchStart, etc.) | Você precisa de dados de multi-toque (touches, changedTouches) | Um único ponteiro é suficiente |
| API de Arrastar e Soltar do HTML | Você precisa de arrastar e soltar nativo com transferência de arquivo/dados | Você precisa de animações de arrastar personalizadas e suaves |
Bibliotecas de Gestos (use-gesture) | Você precisa de gestos complexos com momento, inércia, restrições | Um clique/arrastar simples é suficiente |
CSS :hover / :active | Estados visuais apenas de hover/press | Você precisa de lógica JS baseada em hover/press |
touch-action: none como propriedade CSS ao elemento arrastávelpointermove nativo com { passive: false } e chame preventDefault()setPointerCapture redireciona todos os eventos futuros para aquele ponteiro para o elemento de capturaonPointerDown e ele libera automaticamente em pointerupfunction handlePointer(e: React.PointerEvent) {
if (e.pointerType === "mouse") { /* entrada de mouse */ }
if (e.pointerType === "touch") { /* toque de dedo */ }
if (e.pointerType === "pen") { /* estilete/caneta */ }
}pointerleave dispara e você para de receber pointermovesetPointerCapture(e.pointerId) em onPointerDown para continuar recebendo eventosRevisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥