Skip to Content

Chapitre 3.3 — Pressable & le tactile

Où on en est : vous savez afficher. Maintenant, interagir — et découvrir que le doigt n’est pas une souris : pas de hover, pas de curseur, des cibles de 8 mm et un besoin vital de feedback.

⏱️ TL;DRPressable est LE composant d’interaction moderne (oubliez TouchableOpacity des vieux tutos, et le Button core trop rigide). Il expose l’état pressed pour styler le feedback, le ripple Android natif, hitSlop pour agrandir les petites cibles, et les événements du cycle tactile. On construit AnswerButton, le bouton de réponse de Drill — avec ses états correct/incorrect.

🎯 Objectifs

  • Utiliser Pressable avec un style fonction de l’état pressed.
  • Donner un feedback Android idiomatique (ripple) et des cibles ≥ 44 pt (hitSlop).
  • Distinguer onPress, onPressIn/Out, onLongPress (et delayLongPress).
  • Construire le composant AnswerButton de Drill, accessible.

Du onClick au onPress

import { Pressable, Text } from 'react-native' <Pressable onPress={() => checkAnswer(choice)} style={({ pressed }) => [styles.btn, pressed && styles.btnPressed]} > <Text style={styles.label}>{choice.text}</Text> </Pressable>

Deux nouveautés par rapport au web :

  1. style peut être une fonction recevant l’état — pressed remplace :hover/:active. Un tableau de styles = composition (le second surcharge le premier).
  2. Le cycle tactile est plus riche qu’un clic : onPressIn (doigt posé) → onPressOut (doigt levé) → onPress (tap validé) ; onLongPress si le doigt reste (~500 ms, réglable via delayLongPress).

⚠️ Piège — Les tutoriels pré-2021 utilisent TouchableOpacity / TouchableHighlight. Ils fonctionnent encore, mais Pressable les remplace tous : API unifiée, état exposé, plus extensible. Quant au composant Button core, il ne se style quasiment pas — on ne l’utilise jamais dans une vraie app. Dans ce cours : Pressable, exclusivement.

Le feedback : non négociable

Sur le web, le hover annonce l’interactivité. En mobile, rien n’annonce rien : sans feedback au toucher, l’utilisateur ne sait pas si son tap a été pris. Deux écoles :

// 1. Feedback par style (cross-platform, contrôlé) style={({ pressed }) => [styles.btn, pressed && { opacity: 0.7, transform: [{ scale: 0.98 }] }]} // 2. Ripple Android natif (l'onde du Material Design) <Pressable android_ripple={{ color: 'rgba(88, 196, 220, 0.25)', borderless: false }} style={styles.btn} >

android_ripple donne l’onde native — le feedback que vos doigts Android connaissent par cœur. Drill l’utilise, avec l’opacité en complément.

🍏 Côté iOSandroid_ripple est ignoré sur iPhone (pas de ripple dans le langage iOS) : le style fonction pressed y reste votre seul feedback. Nos composants combinent les deux — c’est ça, écrire cross-platform sans effort.

Les cibles tactiles : 44 pt minimum

Un doigt couvre ~8 mm : les guidelines (Android comme Apple) imposent des cibles d’au moins 44-48 dp. Deux outils :

// Agrandir la zone SANS agrandir le visuel : hitSlop <Pressable hitSlop={12} onPress={dismiss}> <Text style={{ fontSize: 14 }}>✕</Text> </Pressable>

hitSlop={12} étend la zone tactile de 12 dp dans chaque direction. L’autre outil est simplement le padding — qui, lui, agrandit aussi le visuel. Règle Drill : tout élément interactif fait au moins 48 dp de haut.

AnswerButton — le composant central de Drill

Le bouton de réponse a 4 états visuels : neutre, pressé, correct (vert), incorrect (rouge) — les deux derniers après validation :

// components/AnswerButton.tsx import { Pressable, StyleSheet, Text } from 'react-native' type AnswerState = 'idle' | 'correct' | 'incorrect' | 'disabled' type Props = { label: string state?: AnswerState onPress: () => void } export function AnswerButton({ label, state = 'idle', onPress }: Props) { const interactive = state === 'idle' return ( <Pressable onPress={onPress} disabled={!interactive} android_ripple={interactive ? { color: 'rgba(88,196,220,0.2)' } : undefined} accessibilityRole="button" accessibilityState={{ disabled: !interactive }} style={({ pressed }) => [ styles.base, state === 'correct' && styles.correct, state === 'incorrect' && styles.incorrect, state === 'disabled' && styles.dimmed, pressed && interactive && styles.pressed, ]} > <Text style={styles.label}>{label}</Text> </Pressable> ) } const styles = StyleSheet.create({ base: { minHeight: 56, // cible généreuse justifyContent: 'center', paddingHorizontal: 16, borderRadius: 12, borderWidth: 1, borderColor: '#1e3a45', backgroundColor: '#101b21', }, pressed: { opacity: 0.85, transform: [{ scale: 0.99 }] }, correct: { borderColor: '#22c55e', backgroundColor: 'rgba(34,197,94,0.12)' }, incorrect: { borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.12)' }, dimmed: { opacity: 0.45 }, label: { color: '#e2e8f0', fontSize: 16, fontWeight: '600' }, })

Points d’architecture :

  • L’état vient du parent (state en prop) : le bouton est bête, la logique de jeu (Partie 6) décidera qui est correct/incorrect. C’est votre réflexe React habituel — il vaut aussi ici.
  • disabled coupe les interactions après validation (pas de double réponse).
  • accessibilityRole / accessibilityState : TalkBack (le lecteur d’écran Android) annoncera « bouton, désactivé ». Quatre lignes pour une app accessible — on les écrit toujours.

📱 Essaie sur ton téléphone — Montez un mini-écran avec une question en dur et 4 AnswerButton. Au tap, passez le bon en correct, les autres en disabled. Vous venez d’écrire le cœur visuel de Drill — sans encore le savoir-faire d’état (Partie 6 le rendra propre).

✏️ Exercices

1. Ajoutez à AnswerButton un retour haptique léger au tap (anticipation de la Partie 9 : expo-haptics est déjà dans Expo Go).

✅ Solution

import * as Haptics from 'expo-haptics' onPress={() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) onPress() }}

(npx expo install expo-haptics.) Une vibration de 10 ms au tap — le genre de détail qui rend une app « native au toucher ». La Partie 9 systématise.

2. Créez un IconButton de 24 dp visuels mais conforme aux 48 dp de cible minimale, de deux façons différentes.

✅ Solution

(1) hitSlop={12} : 24 + 12×2 = 48 dp de zone tactile, visuel inchangé. (2) padding: 12 sur le Pressable (48 dp visuels et tactiles) avec l’icône de 24 dp centrée. hitSlop préserve le layout serré ; le padding est plus explicite — les deux sont valides.

3. Pourquoi met-on android_ripple à undefined quand le bouton n’est pas interactif ?

✅ Solution

Un ripple sur un élément désactivé est un mensonge tactile : il promet une interaction qui n’aura pas lieu. disabled coupe onPress mais pas le ripple — on retire donc le ripple explicitement. Cohérence feedback ↔ comportement.

🧠 Quiz

1. Pourquoi Pressable plutôt que TouchableOpacity ou Button ?

Réponse

Pressable est l’API moderne unifiée : état pressed exposé au style, android_ripple, hitSlop, cycle tactile complet. TouchableOpacity = héritage pré-2021 ; Button core = rigide, quasi non stylable.

2. Quel est l’ordre des événements d’un tap long ?

Réponse

onPressIn (contact) → onLongPress (seuil ~500 ms atteint) → onPressOut (doigt levé). onPress n’est PAS déclenché si onLongPress a eu lieu.

3. Comment styler l’état pressé d’un Pressable ?

Réponse

En passant une fonction à style : style={({ pressed }) => [styles.base, pressed && styles.pressed]} — l’équivalent tactile de :active.

4. Que fait hitSlop et quand l’utiliser ?

Réponse

Il étend la zone tactile au-delà du visuel (ex. hitSlop={12}). À utiliser quand un élément visuellement petit (croix de fermeture, icône) doit atteindre les 44-48 dp minimum.

5. Citez les deux props d’accessibilité minimales d’un bouton custom.

Réponse

accessibilityRole="button" (annonce le rôle à TalkBack) et accessibilityState={{ disabled }} (annonce l’état). Ajouter accessibilityLabel si le contenu visuel ne parle pas de lui-même.


👉 Chapitre suivant : 3.4 — FlatList — le composant le plus important de React Native, et le piège n°1 des devs web.