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;DR —
Pressableest LE composant d’interaction moderne (oubliez TouchableOpacity des vieux tutos, et leButtoncore trop rigide). Il expose l’étatpressedpour styler le feedback, le ripple Android natif,hitSloppour agrandir les petites cibles, et les événements du cycle tactile. On construitAnswerButton, 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
AnswerButtonde 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 :
stylepeut être une fonction recevant l’état —pressedremplace:hover/:active. Un tableau de styles = composition (le second surcharge le premier).- Le cycle tactile est plus riche qu’un clic :
onPressIn(doigt posé) →onPressOut(doigt levé) →onPress(tap validé) ;onLongPresssi le doigt reste (~500 ms, réglable viadelayLongPress).
⚠️ 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 composantButtoncore, 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é iOS —
android_rippleest ignoré sur iPhone (pas de ripple dans le langage iOS) : le style fonctionpressedy 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 (
stateen 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. disabledcoupe 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 encorrect, les autres endisabled. 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.