Manejo de eventos y Touchables
Pressable, gestos en la capa primitiva, y patrones de hitSlop.
Busca en todas las páginas de la documentación
Pressable, gestos en la capa primitiva, y patrones de hitSlop.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
import { Pressable, StyleSheet, Text, View } from "react-native";
interface IconButtonProps {
label: string;
onPress: () => void;
disabled?: boolean;
}
export function IconButton({ label, onPress, disabled = false }: IconButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
hitSlop={12}
accessibilityRole="button"
accessibilityState={{ disabled }}
style={({ pressed }) => [
styles.button,
pressed && !disabled && styles.pressed,
disabled && styles.disabled,
]}
android_ripple={{ color: "rgba(255,255,255,0.25)", borderless: false }}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
minHeight: 44,
minWidth: 44,
paddingHorizontal: 16,
borderRadius: 10,
backgroundColor: "#2563eb",
alignItems: "center",
justifyContent: "center",
},
pressed: { opacity: 0.85 },
disabled: { opacity: 0.4 },
label: { color: "#fff", fontWeight: "600", fontSize: 15 },
});Cuándo usarlo: Cualquier control tocable - botones, filas de lista, chips - donde necesites presses confiables, objetivos accesibles y retroalimentación apropiada para la plataforma.
import { useState } from "react";
import {
Platform,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
type Action = "save" | "share" | "delete";
const ACTIONS: { key: Action; label: string; destructive?: boolean }[] = [
{ key: "save", label: "Guardar borrador" },
{ key: "share", label: "Compartir enlace" },
{ key: "delete", label: "Eliminar", destructive: true },
];
export default function ActionSheetDemo() {
const [lastEvent, setLastEvent] = useState("Toca una acción");
const [pressedKey, setPressedKey] = useState<Action | null>(null);
return (
<View style={styles.screen}>
<Text style={styles.title}>Objetivos de toque</Text>
<Text style={styles.subtitle}>{lastEvent}</Text>
<View style={styles.sheet}>
{ACTIONS.map((action) => (
<Pressable
key={action.key}
onPress={() => setLastEvent(`onPress: ${action.label}`)}
onPressIn={() => setPressedKey(action.key)}
onPressOut={() => setPressedKey(null)}
onLongPress={() => setLastEvent(`onLongPress: ${action.label}`)}
delayLongPress={400}
hitSlop={{ top: 6, bottom: 6, left: 4, right: 4 }}
accessibilityRole="button"
accessibilityHint={
action.destructive ? "Elimina permanentemente el elemento" : undefined
}
style={({ pressed }) => [
styles.row,
pressed && styles.rowPressed,
action.destructive && styles.rowDestructive,
]}
android_ripple={{
color: action.destructive ? "rgba(239,68,68,0.2)" : "rgba(37,99,235,0.15)",
}}
>
<Text
style={[
styles.rowLabel,
action.destructive && styles.destructiveText,
pressedKey === action.key && styles.rowLabelPressed,
]}
>
{action.label}
</Text>
<Text style={styles.chevron}>›</Text>
</Pressable>
))}
</View>
<View style={styles.toolbar}>
<Pressable
onPress={() => setLastEvent("Icono de barra tocado")}
hitSlop={16}
style={({ pressed }) => [styles.iconHit, pressed && styles.iconHitPressed]}
>
<View style={styles.iconGlyph} />
</Pressable>
<Text style={styles.toolbarHint}>
El icono es 24×24 con 16pt hitSlop - objetivo efectivo de 56×56
</Text>
</View>
{Platform.OS === "ios" && (
<Text style={styles.note}>
iOS: retroalimentación de opacidad; Android: ripple vía android_ripple
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 20, backgroundColor: "#f8fafc", gap: 16 },
title: { fontSize: 22, fontWeight: "700", color: "#0f172a" },
subtitle: { fontSize: 15, color: "#475569" },
sheet: {
backgroundColor: "#fff",
borderRadius: 14,
overflow: "hidden",
borderWidth: StyleSheet.hairlineWidth,
borderColor: "#cbd5e1",
},
row: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 14,
minHeight: 48,
},
rowPressed: { backgroundColor: "#f1f5f9" },
rowDestructive: {},
rowLabel: { flex: 1, fontSize: 16, color: "#0f172a" },
rowLabelPressed: { color: "#2563eb" },
destructiveText: { color: "#dc2626" },
chevron: { fontSize: 20, color: "#94a3b8" },
toolbar: { flexDirection: "row", alignItems: "center", gap: 12 },
iconHit: { padding: 4 },
iconHitPressed: { opacity: 0.6 },
iconGlyph: {
width: 24,
height: 24,
borderRadius: 6,
backgroundColor: "#2563eb",
},
toolbarHint: { flex: 1, fontSize: 13, color: "#64748b" },
note: { fontSize: 12, color: "#94a3b8" },
});Qué demuestra esto:
Pressable reaccionando al estado pressed para retroalimentación multiplataformaonPressIn / onPressOut para rastrear presses activos sin disparar el press completoonLongPress con delayLongPress para acciones secundariashitSlop ampliando objetivos de iconos pequeños sin cambiar métricas de layoutandroid_ripple para ripples táctiles de estilo Material en AndroidaccessibilityRole, accessibilityHint y accessibilityState en elementos interactivosPressable rastrea los estados del ciclo de vida del press (pressed, hovered, focused) y los mapea a estilos o funciones de children.onPress se dispara en la liberación del toque dentro de los límites de la vista cuando el gesto no es cancelado.disabled elimina la vista de la cadena de respondedores para la activación de press.react-native-gesture-handler en lugar de APIs respondedoras crudas.| Evento | Se dispara cuando | Uso típico |
|---|---|---|
onPressIn | Dedo dentro de la vista | Destacar, preparación háptica |
onPressOut | Dedo arriba o gesto cancelado | Limpiar destacado |
onPress | Tap completado (abajo + arriba dentro) | Acción principal |
onLongPress | Mantenido más allá de delayLongPress (predeterminado 500ms) | Menú de contexto, confirmación de eliminación |
onPress no disparado | Scroll roba el gesto, dedo se desliza afuera, padre se desplaza | Esperado - no luches contra scroll |
// Expansión uniforme (número = todos los lados)
<Pressable hitSlop={12} />
// Control por borde - útil arriba de barras de pestañas o debajo de muescas
<Pressable hitSlop={{ top: 8, bottom: 16, left: 12, right: 12 }} />
// hitSlop negativo encoge el objetivo (raro - filas superpuestas)
<Pressable hitSlop={-4} />| Tamaño de objetivo | Mínimo recomendado | Técnica |
|---|---|---|
| Enlace de texto | 44×44 pt efectivo | hitSlop + minHeight |
| Icono de barra 24×24 | 44×44 pt efectivo | hitSlop={10} o wrapper de relleno |
| Fila de lista de ancho completo | 48dp altura | paddingVertical en Pressable |
| Acción destructiva | 44×44 + confirmación | onLongPress o segundo paso |
| Componente | Retroalimentación | Estado |
|---|---|---|
Pressable | Opacidad personalizada, ripple, función de children | Preferido |
TouchableOpacity | Desvanece opacidad en press | Heredado, aún común |
TouchableHighlight | Oscurece underlay | Heredado |
TouchableWithoutFeedback | Ninguno | Evitar en botones - brechas a11y |
// Capa primitiva - suficiente para botones y filas de lista
<Pressable onPress={save} />
// Gesture Handler - pan, pinch, manejadores simultáneos (instalar en Expo)
import { Gesture, GestureDetector } from "react-native-gesture-handler";
const pan = Gesture.Pan().onEnd((e) => {
if (e.translationX < -80) archiveItem();
});Pressable hasta que aparezcan conflictos de scroll o requisitos multi-toque.react-native-gesture-handler se integra con drivers nativos para pans a 60fps.import type { PressableProps, StyleProp, ViewStyle } from "react-native";
import type { GestureResponderEvent } from "react-native";
type ButtonStyle = PressableProps["style"]; // StyleProp<ViewStyle> | fn
interface RowProps {
onPress: (event: GestureResponderEvent) => void;
style?: StyleProp<ViewStyle>;
}
// Forma de función de estilo
const styleFn: Extract<ButtonStyle, Function> = ({ pressed, hovered }) => [
styles.base,
pressed && styles.pressed,
];PressableProps["style"] cubre tanto estilos estáticos como la forma callback pressed.GestureResponderEvent tipifica metadatos nativos de toque (nativeEvent.locationX).() => void explícitos en props cuando no necesites el objeto de evento.Objetivos de toque diminutos - Un icono de 20×20 sin hitSlop no cumple con los mínimos HIG/Material y frustra a los usuarios. Solución: Añade hitSlop o envuelve con relleno para alcanzar ~44×44 pt.
ScrollView roba presses - Los deslizamientos horizontales en un Pressable dentro de ScrollView pueden cancelar onPress. Solución: Ajusta keyboardShouldPersistTaps, usa delayPressIn, o mueve el gesto a Gesture Handler.
TouchableOpacity envolviendo árboles grandes - La desvanecimiento de opacidad en un subárbol enorme destella todos los children. Solución: Aplica retroalimentación solo en la fila Pressable, no en el wrapper de pantalla completa.
Retroalimentación disabled faltante - disabled silencia onPress pero los visuales pueden parecer activos. Solución: Combina estilo disabled, accessibilityState={{ disabled: true }} y pointerEvents donde sea necesario.
Pressables superpuestos pelean - Los overlays absolutos interceptan toques involuntariamente. Solución: Establece pointerEvents="box-none" en padres decorativos o pointerEvents="none" en overlays.
Confiar en onPress para detección de arrastre - Press se dispara después de la liberación; los gestos de arrastre necesitan manejadores pan. Solución: Usa react-native-gesture-handler Pan para swipe-to-delete.
Android ripple sin overflow hidden - Los ripples se desbordan de tarjetas redondeadas. Solución: Establece overflow: "hidden" en la tarjeta y coincide borderRadius en android_ripple.
| Alternativa | Usalo cuando | No lo uses cuando |
|---|---|---|
Pressable | Botones predeterminados, filas de lista, chips | Gestos multi-dedo complejos |
TouchableOpacity | Manteniendo código heredado rápidamente | Greenfield - prefiere Pressable |
Text onPress | Enlaces en línea dentro de copia | CTAs principales que necesitan objetivos de 44pt |
react-native-gesture-handler | Filas de deslizamiento, cajones, pinch zoom | Botones estáticos simples |
Button nativo | Acciones triviales con estilo de sistema | Estilos de marca personalizados |
expo-haptics + Pressable | Confirmación táctil en éxito | Cada tap - causa fatiga háptica |
Pressable expone los estados pressed, hovered y focused en una función de estilo.android_ripple, hitSlop y props de accesibilidad integrados son de primera clase.TouchableOpacity siempre anima opacidad - menos control y superficie de re-renderizado más amplia en árboles grandes.ScrollView padre antes de la liberación.disabled={true} está establecido en el Pressable.<Pressable
android_ripple={{ color: "rgba(0,0,0,0.12)" }}
style={({ pressed }) => [styles.btn, pressed && styles.btnPressed]}
/>android_ripple se ignora en iOS - seguro de enviar en archivos multiplataforma.pressed (típicamente opacidad más baja o cambio de fondo).delayPressIn (predeterminado 0) puede ser elevado por padres o retención de press de ScrollView.onPressIn bloquea el siguiente frame.onPress o requestAnimationFrame; muestra destacado inmediatamente.onLongPress (predeterminado 500).onPress se dispara en taps rápidos a menos que el manejador long-press consuma el gesto.auto (predeterminado) - la vista y los children reciben toques.none - la vista y los children son transparentes a toques.box-none - la vista ignora toques; los children aún los reciben.box-only - la vista recibe toques; los children no.Pressable para código nuevo - estado y control de ripple más claros.TouchableHighlight puede destellar incorrectamente en listas virtualizadas durante scroll rápido.onPress por elemento.const [submitting, setSubmitting] = useState(false);
const onSubmit = async () => {
if (submitting) return;
setSubmitting(true);
try {
await save();
} finally {
setSubmitting(false);
}
};
<Pressable onPress={onSubmit} disabled={submitting} />Pressable de fila con nodos Text hijo.accessibilityRole="button" marca el elemento para lectores de pantalla.accessibilityLabel cuando el texto visible es insuficiente (solo icono).accessibilityState={{ disabled, selected, busy }} refleja el estado visual.accessibilityHint para resultados destructivos u obvios.GestureDetector para casos avanzados.Pressable se ejecutan en el hilo JavaScript (Hermes).react-native-reanimated pueden mantenerse en el hilo de UI por separado.TextInput, keyboardShouldPersistTaps="handled" permite que los taps en Pressable se disparen sin descartar primero el teclado.hitSlop - ambos resuelven problemas diferentes.hitSlop modesto.Pressable; ajusta cuando los usuarios informan cancelaciones de "slip off".onPressVersiones de Stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026