Eventos de Toque
Lide com interações de toque em dispositivos móveis -- toques, deslizes, gestos de pinça e multitoque.
Busque em todas as páginas da documentação
Lide com interações de toque em dispositivos móveis -- toques, deslizes, gestos de pinça e multitoque.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Evento | Dispara Quando | Objeto de Evento |
|---|---|---|
onTouchStart | Um dedo toca a tela | React.TouchEvent |
onTouchMove | Um dedo se move enquanto toca a tela | React.TouchEvent |
onTouchEnd | Um dedo solta a tela | React.TouchEvent |
onTouchCancel | O sistema cancela o toque (por exemplo, chamada recebida, conflito de gestos) | React.TouchEvent |
Cada React.TouchEvent expõe:
| Propriedade | Tipo | Descrição |
|---|---|---|
touches | TouchList | Todos os dedos atualmente na tela |
targetTouches | TouchList | Dedos no elemento de destino atual |
changedTouches | TouchList | Dedos envolvidos no evento atual |
Cada objeto Touch contém: identifier, clientX, clientY, screenX, screenY, pageX, pageY, target.
Cartão de receita de referência rápida -- pronto para copiar e colar.
// SwipeHandler.tsx -- detecção básica de deslize
"use client";
import { useRef, useCallback } from "react";
export function useSwipe(onSwipe: (direction: "left" | "right" | "up" | "down") => void) {
const startX = useRef(0);
const startY = useRef(0);
const handleTouchStart = useCallback((e: React.TouchEvent) => {
startX.current = e.changedTouches[0].clientX;
startY.current = e.changedTouches[0].clientY;
}, []);
const handleTouchEnd = useCallback(
(e: React.TouchEvent) => {
const dx = e.changedTouches[0].clientX - startX.current;
const dy = e.changedTouches[0].clientY - startY.current;
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
const threshold = 50;
if (Math.max(absDx, absDy) < threshold) return;
if (absDx > absDy) {
onSwipe(dx > 0 ? "right" : "left");
} else {
onSwipe(dy > 0 ? "down" : "up");
}
},
[onSwipe]
);
return { onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd };
}Quando usar isso: Você precisa detectar gestos de deslize em dispositivos móveis e não pode depender de scroll-snap do CSS ou de uma biblioteca de gestos.
// SwipeableCards.tsx
"use client";
import { useState, useCallback } from "react";
type Card = { id: number; title: string; color: string };
const cards: Card[] = [
{ id: 1, title: "Bem-vindo", color: "#3b82f6" },
{ id: 2, title: "Recursos", color: "#10b981" },
{ id: 3, title: "Preços", color: "#f59e0b" },
{ id: 4, title: "Comece", color: "#ef4444" },
];
export default function SwipeableCards() {
const [currentIndex, setCurrentIndex] = useState(0);
const [touchStart, setTouchStart] = useState<number | null>(null);
const [touchDelta, setTouchDelta] = useState(0);
const handleTouchStart = useCallback((e: React.TouchEvent) => {
setTouchStart(e.changedTouches[0].clientX);
setTouchDelta(0);
}, []);
const handleTouchMove = useCallback(
(e: React.TouchEvent) => {
if (touchStart === null) return;
const current = e.changedTouches[0].clientX;
setTouchDelta(current - touchStart);
},
[touchStart]
);
const handleTouchEnd = useCallback(() => {
const threshold = 80;
if (touchDelta < -threshold && currentIndex < cards.length - 1) {
setCurrentIndex((prev) => prev + 1);
} else if (touchDelta > threshold && currentIndex > 0) {
setCurrentIndex((prev) => prev - 1);
}
setTouchStart(null);
setTouchDelta(0);
}, [touchDelta, currentIndex]);
return (
<div className="overflow-hidden w-full max-w-sm mx-auto">
<div
className="flex transition-transform duration-300 ease-out"
style={{
transform: `translateX(calc(-${currentIndex * 100}% + ${touchStart !== null ? touchDelta : 0}px))`,
transition: touchStart !== null ? "none" : "transform 0.3s ease-out",
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{cards.map((card) => (
<div
key={card.id}
className="min-w-full p-8 rounded-xl text-white text-center text-2xl font-bold"
style={{ backgroundColor: card.color }}
>
{card.title}
</div>
))}
</div>
<div className="flex justify-center gap-2 mt-4">
{cards.map((_, i) => (
<div
key={i}
className={`w-2 h-2 rounded-full ${i === currentIndex ? "bg-gray-800" : "bg-gray-300"}`}
/>
))}
</div>
</div>
);
}O que isso demonstra:
onTouchStart, onTouchMove e onTouchEnd juntos para deslizar e navegartouchstart -> touchmove (repetido) -> touchend ou touchcancel.SyntheticEvent para que eles se comportem de forma consistente entre os navegadores.identifier exclusivo que persiste de touchstart a touchend, permitindo o rastreamento de multitoque.changedTouches contém apenas os toques que mudaram no evento atual, enquanto touches contém todos os toques ativos.Detecção de deslize para a esquerda/direita com velocidade:
"use client";
import { useRef, useCallback } from "react";
export function useSwipeWithVelocity(
onSwipe: (dir: "left" | "right", velocity: number) => void
) {
const startRef = useRef({ x: 0, time: 0 });
const handleTouchStart = useCallback((e: React.TouchEvent) => {
startRef.current = {
x: e.changedTouches[0].clientX,
time: Date.now(),
};
}, []);
const handleTouchEnd = useCallback(
(e: React.TouchEvent) => {
const dx = e.changedTouches[0].clientX - startRef.current.x;
const dt = Date.now() - startRef.current.time;
const velocity = Math.abs(dx) / dt; // px por ms
// Deslize rápido (alta velocidade) requer menos distância
const threshold = velocity > 0.5 ? 30 : 80;
if (Math.abs(dx) < threshold) return;
onSwipe(dx > 0 ? "right" : "left", velocity);
},
[onSwipe]
);
return { onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd };
}Pinça para zoom:
"use client";
import { useState, useRef, useCallback } from "react";
function getDistance(t1: React.Touch, t2: React.Touch): number {
return Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
}
export function PinchZoomImage({ src, alt }: { src: string; alt: string }) {
const [scale, setScale] = useState(1);
const initialDistance = useRef(0);
const initialScale = useRef(1);
const handleTouchStart = useCallback(
(e: React.TouchEvent) => {
if (e.touches.length === 2) {
initialDistance.current = getDistance(e.touches[0], e.touches[1]);
initialScale.current = scale;
}
},
[scale]
);
const handleTouchMove = useCallback((e: React.TouchEvent) => {
if (e.touches.length === 2) {
e.preventDefault();
const currentDistance = getDistance(e.touches[0], e.touches[1]);
const ratio = currentDistance / initialDistance.current;
setScale(Math.min(Math.max(initialScale.current * ratio, 0.5), 4));
}
}, []);
return (
<div
className="overflow-hidden touch-none"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
>
<img
src={src}
alt={alt}
className="w-full transition-transform"
style={{ transform: `scale(${scale})` }}
/>
</div>
);
}Pressionamento longo em dispositivos móveis:
"use client";
import { useRef, useCallback } from "react";
export function useLongPress(onLongPress: () => void, delay = 500) {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const start = useCallback(
(e: React.TouchEvent) => {
e.preventDefault();
timerRef.current = setTimeout(onLongPress, delay);
},
[onLongPress, delay]
);
const cancel = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
return {
onTouchStart: start,
onTouchEnd: cancel,
onTouchMove: cancel,
onTouchCancel: cancel,
};
}Toque vs clique -- lidando com ambos sem disparo duplo:
"use client";
import { useRef, useCallback } from "react";
export function useTapOrClick(handler: () => void) {
const touchedRef = useRef(false);
const onTouchEnd = useCallback(
(e: React.TouchEvent) => {
touchedRef.current = true;
e.preventDefault(); // impede o evento de clique subsequente
handler();
},
[handler]
);
const onClick = useCallback(() => {
// Dispara apenas em dispositivos sem toque, pois o preventDefault do toque o interrompe
if (!touchedRef.current) {
handler();
}
touchedRef.current = false;
}, [handler]);
return { onTouchEnd, onClick };
}Impedindo a rolagem durante a interação de toque:
"use client";
import { useEffect, useRef } from "react";
export function usePreventScrollDuringTouch(active: boolean) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el || !active) return;
const prevent = (e: TouchEvent) => e.preventDefault();
// Deve usar { passive: false } para permitir preventDefault
el.addEventListener("touchmove", prevent, { passive: false });
return () => el.removeEventListener("touchmove", prevent);
}, [active]);
return ref;
}// O tipo de evento principal
function handleTouch(e: React.TouchEvent<HTMLDivElement>) {
// changedTouches é um TouchList -- semelhante a um array, não um array real
const touch: React.Touch = e.changedTouches[0];
const x: number = touch.clientX;
const y: number = touch.clientY;
const id: number = touch.identifier;
// Converte TouchList em array para iteração
const allTouches: React.Touch[] = Array.from(e.touches);
}
// Tipagem genérica de elemento
function handleTouchOnCanvas(e: React.TouchEvent<HTMLCanvasElement>) {
const canvas = e.currentTarget; // tipado como HTMLCanvasElement
const rect = canvas.getBoundingClientRect();
const touch = e.changedTouches[0];
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
}
// Tipo de retorno de hook personalizado
type SwipeHandlers = {
onTouchStart: (e: React.TouchEvent) => void;
onTouchMove: (e: React.TouchEvent) => void;
onTouchEnd: (e: React.TouchEvent) => void;
};
// Rastreamento de identificador de toque entre eventos
const activeTouches = useRef<Map<number, { startX: number; startY: number }>>(
new Map()
);touches está vazio em onTouchEnd -- Quando o último dedo solta, touches e targetTouches estão vazios porque não há mais dedos. Correção: Use changedTouches em onTouchEnd para obter a posição do dedo que acabou de soltar.
e.preventDefault() em manipuladores de toque não funciona com ouvintes passivos -- O React 17+ registra eventos de toque como passivos por padrão na raiz, o que significa que preventDefault() em onTouchMove falha silenciosamente e a página ainda rola. Correção: Use um ref e anexe o ouvinte manualmente com { passive: false }, ou aplique touch-action: none no CSS.
Eventos de toque disparam um clique sintético 300ms depois -- Em navegadores móveis, um touchend é seguido por um click atrasado. Se você manipular ambos onTouchEnd e onClick, o manipulador será acionado duas vezes. Correção: Chame e.preventDefault() no onTouchEnd para suprimir o clique, ou use eventos de ponteiro em vez disso.
TouchList não é um array -- e.touches, e.targetTouches e e.changedTouches são objetos TouchList, não arrays. Chamar .map() ou .filter() diretamente gera um erro. Correção: Converta com Array.from(e.touches) antes de iterar.
Valores de identifier de multitoque não são sequenciais -- Não assuma que os identificadores de toque começam em 0 ou incrementam em 1. O navegador atribui IDs inteiros arbitrários. Correção: Rastreie toques usando um Map<number, TouchData> com a chave identifier.
Acessando propriedades nativeEvent após trabalho assíncrono -- O React agrupa eventos sintéticos. Se você armazenar o evento e lê-lo mais tarde, as propriedades podem ser anuladas. Correção: Extraia os valores de que você precisa de forma síncrona (const x = e.changedTouches[0].clientX) antes de qualquer await ou setState.
Conflito de zoom de pinça com zoom do navegador -- O gesto de pinça com dois dedos aciona o zoom nativo do navegador em muitos navegadores móveis, competindo com seu manipulador de pinça personalizado. Correção: Defina touch-action: none no elemento e adicione <meta name="viewport" content="... user-scalable=no"> (mas esteja ciente de que isso prejudica a acessibilidade).
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
Eventos de Ponteiro (onPointerDown, etc.) | Você deseja manipulação unificada de mouse + toque + caneta | Você precisa especificamente de informações de multitoque como touches e changedTouches |
touch-action do CSS | Você deseja controlar a rolagem/zoom sem JS | Você precisa responder à posição do toque ou dados de gestos |
| Bibliotecas de gestos (use-gesture, Hammer.js) | Você precisa de gestos complexos como rotação, pinça, arremesso com física | Um simples deslize ou toque é suficiente |
onClick apenas | Toques e cliques se comportam da mesma forma para o seu caso de uso | Você precisa de arrastar, deslizar ou multitoque |
scroll-snap do CSS | Você deseja comportamento de carrossel deslizável | Você precisa de controle programático sobre limites de deslize ou animações |
touches -- todos os dedos atualmente na telatargetTouches -- todos os dedos no elemento que originou o toquechangedTouches -- apenas os dedos que mudaram neste evento específico (iniciaram, moveram ou soltaram)onTouchEnd, use changedTouches porque touches não inclui mais o dedo soltopreventDefault()addEventListener("touchmove", handler, { passive: false })touch-action: none no CSS no elementoonTouchStart e compare com a posição final em onTouchEndtouches, changedTouches) que os eventos de ponteiro não possuemfunction handleCanvasTouch(e: React.TouchEvent<HTMLCanvasElement>) {
const rect = e.currentTarget.getBoundingClientRect();
const touch = e.changedTouches[0];
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
// desenhar em (x, y)
}onTouchEnd dispara primeiro, depois um click sintético e às vezes mousedown/mouseupe.preventDefault() em seu manipulador de toque (com um ouvinte não passivo) para suprimir o evento do mouseRevisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥