👆 Pressable: Handling Touch
The modern, flexible way to make anything touchable in React Native
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Understand why Pressable is the recommended touch handler
- Use all press states: onPressIn, onPressOut, onPress, and onLongPress
- Create dynamic visual feedback using the style function
- Improve touch targets with hitSlop and pressRetentionOffset
- Migrate from legacy touchable components to Pressable
- Build accessible, responsive touch interactions
⏱️ Estimated Time: 25-35 minutes
📑 In This Lesson
Introduction to Pressable
In mobile apps, touch is the primary way users interact with your interface. Unlike web browsers where we have onClick, mobile touch interactions are richer and more nuanced. React Native's Pressable component is the modern, recommended way to handle all touch interactions.
📖 What is Pressable?
Pressable is a core React Native component that detects touch interactions on its children. It provides detailed information about the press lifecycle and allows you to create rich, responsive touch feedback.
Web vs Native Touch
Coming from web development, you might be used to simple click handlers. Mobile touch is different:
🌐 Web Click
- Single event:
onClick - Binary state: clicked or not
- Hover state available
- Mouse precision
📱 Mobile Touch
- Multiple events: press in, out, press, long press
- Rich state: pressing, pressed, released
- No hover (touch required)
- Finger imprecision (need larger targets)
Basic Pressable Usage
import { Pressable, Text, StyleSheet } from 'react-native';
function MyButton() {
return (
<Pressable
onPress={() => console.log('Button pressed!')}
style={styles.button}
>
<Text style={styles.buttonText}>Press Me</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#2196F3',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
✅ Pressable is the Future
React Native introduced Pressable in version 0.63 as a replacement for TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, and TouchableNativeFeedback. While those still work, Pressable is more flexible and is the recommended approach.
What Can Be Pressable?
Anything! Unlike buttons, Pressable wraps any content you want to make touchable:
// A pressable card
<Pressable onPress={() => navigateToDetails(item.id)}>
<View style={styles.card}>
<Image source={{ uri: item.image }} style={styles.cardImage} />
<Text style={styles.cardTitle}>{item.title}</Text>
</View>
</Pressable>
// A pressable icon
<Pressable onPress={() => toggleFavorite()}>
<Text style={styles.icon}>{isFavorite ? '❤️' : '🤍'}</Text>
</Pressable>
// A pressable list item
<Pressable onPress={() => selectItem(item)}>
<View style={styles.listItem}>
<Text>{item.name}</Text>
<Text style={styles.arrow}>→</Text>
</View>
</Pressable>
Understanding Press States
One of Pressable's key features is exposing the full touch lifecycle through multiple callbacks. Understanding these states is crucial for creating polished, responsive interactions.
The four press callbacks in order of the touch lifecycle
The Four Press Callbacks
| Callback | When Fired | Common Use |
|---|---|---|
onPressIn |
Finger touches the element | Start visual feedback, animations |
onPressOut |
Finger lifts or leaves element | End visual feedback, animations |
onPress |
After onPressOut (complete press) | Primary action (navigate, submit) |
onLongPress |
After holding ~500ms | Secondary action (context menu) |
Using All Press States
import { Pressable, Text, View, StyleSheet } from 'react-native';
import { useState } from 'react';
function PressStateDemo() {
const [log, setLog] = useState<string[]>([]);
const addLog = (event: string) => {
setLog(prev => [...prev.slice(-4), `${new Date().toLocaleTimeString()}: ${event}`]);
};
return (
<View style={styles.container}>
<Pressable
onPressIn={() => addLog('👇 onPressIn')}
onPressOut={() => addLog('👆 onPressOut')}
onPress={() => addLog('✓ onPress')}
onLongPress={() => addLog('⏱️ onLongPress')}
style={styles.button}
>
<Text style={styles.buttonText}>Press & Hold Me</Text>
</Pressable>
<View style={styles.logContainer}>
{log.map((entry, index) => (
<Text key={index} style={styles.logEntry}>{entry}</Text>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
button: {
backgroundColor: '#667eea',
padding: 20,
borderRadius: 12,
alignItems: 'center',
},
buttonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
logContainer: {
marginTop: 20,
padding: 16,
backgroundColor: '#f5f5f5',
borderRadius: 8,
minHeight: 150,
},
logEntry: {
fontFamily: 'monospace',
fontSize: 12,
marginVertical: 2,
},
});
Event Sequence Examples
Quick Tap
onPressIn— finger downonPressOut— finger uponPress— action triggered
Long Press
onPressIn— finger downonLongPress— after 500msonPressOut— finger uponPress— (if not cancelled)
Press & Slide Away
onPressIn— finger downonPressOut— finger left area- No onPress! — cancelled
Customizing Long Press Duration
// Default is 500ms
<Pressable
onLongPress={() => showContextMenu()}
delayLongPress={300} // Trigger after 300ms instead
>
// Different delays for different interactions
<Pressable
onLongPress={() => enterEditMode()}
delayLongPress={1000} // Require full second hold
>
Preventing onPress After Long Press
Sometimes you want long press to be the only action, without triggering the regular press:
import { useState, useRef } from 'react';
function LongPressOnly() {
const didLongPress = useRef(false);
const handleLongPress = () => {
didLongPress.current = true;
showContextMenu();
};
const handlePress = () => {
if (didLongPress.current) {
didLongPress.current = false;
return; // Don't trigger regular press
}
navigateToItem();
};
const handlePressIn = () => {
didLongPress.current = false;
};
return (
<Pressable
onPressIn={handlePressIn}
onPress={handlePress}
onLongPress={handleLongPress}
>
<Text>Tap to open, hold for options</Text>
</Pressable>
);
}
⚠️ Order Matters
onPress fires after onPressOut, even for long presses. If you need to prevent onPress after a long press, you need to track it manually as shown above.
Press State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Pressed: onPressIn
(finger down)
Pressed --> Idle: onPressOut + onPress
(quick tap)
Pressed --> LongPressed: 500ms elapsed
(onLongPress)
LongPressed --> Idle: onPressOut
(finger up)
Pressed --> Cancelled: finger slides away
(onPressOut only)
Cancelled --> Idle
note right of Pressed: Visual feedback active
note right of LongPressed: Long press action triggered
note right of Cancelled: No onPress fires
State transitions during touch interaction
Visual Feedback with Style Function
Users need immediate feedback when they touch something. Without it, the app feels broken or slow. Pressable's killer feature is the ability to pass a function to the style prop that receives the current press state.
💡 Key Insight
Unlike static styles, Pressable accepts a style function that receives { pressed } and returns styles. This lets you create dynamic visual feedback without managing state yourself.
The Style Function Pattern
import { Pressable, Text, StyleSheet } from 'react-native';
function DynamicButton() {
return (
<Pressable
onPress={() => console.log('Pressed!')}
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed
]}
>
{({ pressed }) => (
<Text style={[
styles.buttonText,
pressed && styles.buttonTextPressed
]}>
{pressed ? 'Pressing...' : 'Press Me'}
</Text>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#2196F3',
paddingVertical: 14,
paddingHorizontal: 28,
borderRadius: 10,
alignItems: 'center',
},
buttonPressed: {
backgroundColor: '#1976D2',
transform: [{ scale: 0.98 }],
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
buttonTextPressed: {
opacity: 0.8,
},
});
Understanding the Pressed State
// Style prop can be:
// 1. A static style object
<Pressable style={styles.button}>
// 2. An array of styles
<Pressable style={[styles.button, styles.primaryButton]}>
// 3. A function that returns styles
<Pressable style={({ pressed }) => ({
backgroundColor: pressed ? '#1976D2' : '#2196F3',
opacity: pressed ? 0.8 : 1,
})}>
// 4. A function that returns an array
<Pressable style={({ pressed }) => [
styles.button,
pressed && styles.pressed,
disabled && styles.disabled,
]}>
Different visual feedback techniques: opacity, color, scale, and shadow changes
Practical Feedback Examples
// Opacity fade (most common)
<Pressable
style={({ pressed }) => ({
opacity: pressed ? 0.7 : 1,
})}
>
// Scale down effect
<Pressable
style={({ pressed }) => ({
transform: [{ scale: pressed ? 0.96 : 1 }],
})}
>
// Background color change
<Pressable
style={({ pressed }) => ({
backgroundColor: pressed ? '#1565C0' : '#2196F3',
})}
>
// Combination (professional feel)
<Pressable
style={({ pressed }) => [
styles.button,
{
backgroundColor: pressed ? '#1565C0' : '#2196F3',
transform: [{ scale: pressed ? 0.98 : 1 }],
shadowOpacity: pressed ? 0.1 : 0.3,
}
]}
>
Children as a Function
Just like the style prop, children can be a function too:
import { Pressable, Text, View, StyleSheet } from 'react-native';
function InteractiveCard() {
return (
<Pressable
onPress={() => console.log('Card pressed')}
style={({ pressed }) => [
styles.card,
pressed && styles.cardPressed
]}
>
{({ pressed }) => (
<View>
<Text style={styles.cardTitle}>
{pressed ? '✓ Selected' : 'Tap to Select'}
</Text>
<Text style={[
styles.cardSubtitle,
pressed && { color: '#1565C0' }
]}>
{pressed ? 'Release to confirm' : 'Press and hold for options'}
</Text>
</View>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: 'white',
padding: 20,
borderRadius: 12,
borderWidth: 2,
borderColor: '#e0e0e0',
},
cardPressed: {
borderColor: '#2196F3',
backgroundColor: '#e3f2fd',
},
cardTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 4,
},
cardSubtitle: {
fontSize: 14,
color: '#666',
},
});
Creating Reusable Button Components
import { Pressable, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}
function Button({ title, onPress, variant = 'primary', disabled = false }: ButtonProps) {
const getBackgroundColor = (pressed: boolean) => {
if (disabled) return '#ccc';
const colors = {
primary: { normal: '#2196F3', pressed: '#1976D2' },
secondary: { normal: '#757575', pressed: '#616161' },
danger: { normal: '#f44336', pressed: '#d32f2f' },
};
return pressed ? colors[variant].pressed : colors[variant].normal;
};
return (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => [
styles.button,
{
backgroundColor: getBackgroundColor(pressed),
opacity: disabled ? 0.6 : 1,
transform: [{ scale: pressed && !disabled ? 0.98 : 1 }],
}
]}
>
<Text style={styles.buttonText}>{title}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
});
// Usage:
// <Button title="Save" onPress={handleSave} variant="primary" />
// <Button title="Cancel" onPress={handleCancel} variant="secondary" />
// <Button title="Delete" onPress={handleDelete} variant="danger" />
✅ Pro Tip: Consistent Feedback
Create a reusable button component (like above) that handles all your press feedback consistently. This ensures every button in your app feels the same, which users find reassuring.
HitSlop and Touch Targets
Fingers are imprecise. Apple's Human Interface Guidelines recommend touch targets of at least 44×44 points. But sometimes you can't make the visual element that large. Enter hitSlop.
📖 What is HitSlop?
hitSlop extends the touchable area beyond the visible bounds of a component. The user can touch outside the visible element and still trigger the press. This is invisible to users but makes small elements much easier to tap.
hitSlop makes small elements easier to tap without changing their visual size
Using hitSlop
import { Pressable, Text, StyleSheet } from 'react-native';
// Small close button with extended touch area
function CloseButton({ onClose }: { onClose: () => void }) {
return (
<Pressable
onPress={onClose}
hitSlop={20} // 20px on all sides
style={styles.closeButton}
>
<Text style={styles.closeIcon}>×</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
closeButton: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: '#f0f0f0',
alignItems: 'center',
justifyContent: 'center',
},
closeIcon: {
fontSize: 18,
color: '#666',
fontWeight: 'bold',
},
});
hitSlop Options
// Shorthand: same value all sides
<Pressable hitSlop={20}>
// Object: different values per side
<Pressable hitSlop={{
top: 10,
right: 20,
bottom: 10,
left: 20,
}}>
// Mixed: extend only specific sides
<Pressable hitSlop={{
top: 15,
bottom: 15,
// left and right default to 0
}}>
pressRetentionOffset
Another related prop is pressRetentionOffset. This determines how far the finger can move outside the element before the press is cancelled:
// User can slide finger 30px outside before cancellation
<Pressable
onPress={handlePress}
pressRetentionOffset={{ top: 30, left: 30, right: 30, bottom: 30 }}
>
<Text>Forgiving Button</Text>
</Pressable>
// Useful for buttons where users might "jitter" their touch
// Default is {top: 20, left: 20, right: 20, bottom: 30}
flowchart LR
subgraph hitSlop["hitSlop"]
A[Extends where touch
can START]
end
subgraph pressRetention["pressRetentionOffset"]
B[Extends where touch
can MOVE TO
without cancelling]
end
A --> |"Press starts outside
visible bounds"| C[Touch registers]
B --> |"Finger slides outside
visible bounds"| D[Press still active]
style hitSlop fill:#e3f2fd,stroke:#1976d2
style pressRetention fill:#fff3e0,stroke:#ff9800
hitSlop affects where presses can start; pressRetentionOffset affects where they can move
When to Use hitSlop
✅ Good Use Cases
- Close/dismiss buttons (×)
- Icon-only buttons
- Navigation arrows
- Small action icons
- Checkbox/radio buttons
⚠️ Watch Out
- Don't overlap adjacent touch areas
- Test on actual devices
- Consider left-handed users
- Larger visual targets are still better
Practical Example: Icon Buttons
import { Pressable, View, Text, StyleSheet } from 'react-native';
function IconButton({
icon,
label,
onPress
}: {
icon: string;
label: string;
onPress: () => void;
}) {
return (
<Pressable
onPress={onPress}
hitSlop={15}
style={({ pressed }) => [
styles.iconButton,
pressed && styles.iconButtonPressed
]}
accessibilityLabel={label}
accessibilityRole="button"
>
<Text style={styles.icon}>{icon}</Text>
</Pressable>
);
}
function ActionBar() {
return (
<View style={styles.actionBar}>
<IconButton icon="❤️" label="Like" onPress={() => {}} />
<IconButton icon="💬" label="Comment" onPress={() => {}} />
<IconButton icon="↗️" label="Share" onPress={() => {}} />
<IconButton icon="🔖" label="Save" onPress={() => {}} />
</View>
);
}
const styles = StyleSheet.create({
actionBar: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingVertical: 12,
},
iconButton: {
padding: 8,
},
iconButtonPressed: {
opacity: 0.6,
transform: [{ scale: 0.95 }],
},
icon: {
fontSize: 24,
},
});
Pressable vs Legacy Touchables
Before Pressable, React Native had several touchable components. You'll still see these in older code and tutorials. Understanding them helps you migrate to Pressable.
The Legacy Family
| Component | Feedback | Platform |
|---|---|---|
TouchableOpacity |
Fades opacity on press | Both |
TouchableHighlight |
Darkens background (underlay) | Both |
TouchableWithoutFeedback |
No visual feedback | Both |
TouchableNativeFeedback |
Android ripple effect | Android only |
Why Pressable is Better
🔄 Unified API
One component instead of four. No need to choose between opacity, highlight, or native feedback.
🎨 Full Control
Style function gives you complete control over visual feedback. Create any effect you want.
📱 Cross-Platform
Works identically on iOS and Android. Use android_ripple for native Android feedback when desired.
⚡ Modern API
Better TypeScript support, cleaner props, and actively maintained by React Native team.
Migration Examples
// ❌ Old: TouchableOpacity
import { TouchableOpacity, Text } from 'react-native';
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.7}
>
<Text>Press Me</Text>
</TouchableOpacity>
// ✅ New: Pressable with opacity
import { Pressable, Text } from 'react-native';
<Pressable
onPress={handlePress}
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
>
<Text>Press Me</Text>
</Pressable>
// ❌ Old: TouchableHighlight
import { TouchableHighlight, Text, View } from 'react-native';
<TouchableHighlight
onPress={handlePress}
underlayColor="#ddd"
>
<View style={styles.button}>
<Text>Press Me</Text>
</View>
</TouchableHighlight>
// ✅ New: Pressable with background change
import { Pressable, Text } from 'react-native';
<Pressable
onPress={handlePress}
style={({ pressed }) => [
styles.button,
{ backgroundColor: pressed ? '#ddd' : '#fff' }
]}
>
<Text>Press Me</Text>
</Pressable>
Android Ripple Effect
For the native Android ripple effect, Pressable provides android_ripple:
import { Pressable, Text, StyleSheet, Platform } from 'react-native';
function RippleButton() {
return (
<Pressable
onPress={() => console.log('Pressed')}
android_ripple={{
color: 'rgba(0, 0, 0, 0.2)', // Ripple color
borderless: false, // Contained within bounds
radius: undefined, // Auto-calculate from layout
}}
style={({ pressed }) => [
styles.button,
// Fallback for iOS (no ripple)
Platform.OS === 'ios' && pressed && styles.iosPressed
]}
>
<Text style={styles.buttonText}>Native Ripple on Android</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#2196F3',
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
iosPressed: {
opacity: 0.8,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
💡 android_ripple Options
- color: Ripple color (use rgba for transparency)
- borderless: If true, ripple extends beyond component bounds
- radius: Custom ripple radius (default: auto from layout)
- foreground: If true, ripple renders above children (API 23+)
Cross-Platform Button Component
import { Pressable, Text, StyleSheet, Platform } from 'react-native';
interface CrossPlatformButtonProps {
title: string;
onPress: () => void;
}
function CrossPlatformButton({ title, onPress }: CrossPlatformButtonProps) {
return (
<Pressable
onPress={onPress}
android_ripple={{
color: 'rgba(255, 255, 255, 0.3)',
}}
style={({ pressed }) => [
styles.button,
// Only apply press style on iOS (Android has ripple)
Platform.OS === 'ios' && pressed && styles.buttonPressed,
]}
>
<Text style={styles.buttonText}>{title}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#6200ee',
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
// Android needs overflow hidden for ripple containment
overflow: 'hidden',
},
buttonPressed: {
backgroundColor: '#3700b3',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
});
flowchart TD
A[Using legacy touchable?] --> B{Which one?}
B -->|TouchableOpacity| C[Use Pressable with
opacity style function]
B -->|TouchableHighlight| D[Use Pressable with
backgroundColor change]
B -->|TouchableNativeFeedback| E[Use Pressable with
android_ripple]
B -->|TouchableWithoutFeedback| F[Use Pressable with
no visual feedback]
C --> G[✅ Modern & flexible]
D --> G
E --> G
F --> G
style G fill:#e8f5e9,stroke:#4caf50
Migration path from legacy touchable components to Pressable
Hands-On Exercises
Time to put Pressable into practice! These exercises progress from basic to advanced.
Exercise 1: Basic Press Feedback
Goal: Create a button with visual press feedback.
Requirements:
- Button that changes background color when pressed
- Slight scale reduction on press (0.97)
- Text says "Press Me" normally, "Pressing..." when pressed
💡 Hint
Use both the style function and children function patterns. Remember to use the pressed parameter from both.
✅ Solution
import { Pressable, Text, StyleSheet, View } from 'react-native';
export default function FeedbackButton() {
return (
<View style={styles.container}>
<Pressable
onPress={() => console.log('Button pressed!')}
style={({ pressed }) => [
styles.button,
{
backgroundColor: pressed ? '#1565C0' : '#2196F3',
transform: [{ scale: pressed ? 0.97 : 1 }],
}
]}
>
{({ pressed }) => (
<Text style={styles.buttonText}>
{pressed ? 'Pressing...' : 'Press Me'}
</Text>
)}
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
button: {
paddingVertical: 16,
paddingHorizontal: 32,
borderRadius: 12,
minWidth: 160,
alignItems: 'center',
},
buttonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
});
Exercise 2: Press State Logger
Goal: Create a component that logs all press states.
Requirements:
- Pressable area with all four callbacks: onPressIn, onPressOut, onPress, onLongPress
- Display the last 5 events in a log area below
- Include timestamp with each event
- Different emoji for each event type
💡 Hint
Use useState to manage the log array. Use slice(-5) to keep only the last 5 entries.
✅ Solution
import { useState } from 'react';
import { Pressable, Text, View, StyleSheet, ScrollView } from 'react-native';
type LogEntry = {
id: number;
time: string;
event: string;
emoji: string;
};
export default function PressLogger() {
const [log, setLog] = useState<LogEntry[]>([]);
const [nextId, setNextId] = useState(0);
const addLog = (event: string, emoji: string) => {
const time = new Date().toLocaleTimeString();
setLog(prev => [...prev.slice(-4), { id: nextId, time, event, emoji }]);
setNextId(prev => prev + 1);
};
return (
<View style={styles.container}>
<Pressable
onPressIn={() => addLog('onPressIn', '👇')}
onPressOut={() => addLog('onPressOut', '👆')}
onPress={() => addLog('onPress', '✅')}
onLongPress={() => addLog('onLongPress', '⏱️')}
delayLongPress={500}
style={({ pressed }) => [
styles.pressArea,
pressed && styles.pressAreaActive
]}
>
{({ pressed }) => (
<Text style={styles.pressText}>
{pressed ? '🔵 Pressing...' : '⚪ Press or Hold Me'}
</Text>
)}
</Pressable>
<View style={styles.logContainer}>
<Text style={styles.logTitle}>Event Log:</Text>
<ScrollView style={styles.logScroll}>
{log.length === 0 ? (
<Text style={styles.emptyLog}>No events yet...</Text>
) : (
log.map(entry => (
<Text key={entry.id} style={styles.logEntry}>
{entry.emoji} {entry.time} — {entry.event}
</Text>
))
)}
</ScrollView>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
pressArea: {
backgroundColor: '#e3f2fd',
padding: 40,
borderRadius: 16,
alignItems: 'center',
borderWidth: 3,
borderColor: '#2196F3',
},
pressAreaActive: {
backgroundColor: '#bbdefb',
borderColor: '#1565C0',
},
pressText: {
fontSize: 18,
fontWeight: '600',
color: '#1565C0',
},
logContainer: {
marginTop: 24,
flex: 1,
backgroundColor: '#f5f5f5',
borderRadius: 12,
padding: 16,
},
logTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 12,
color: '#333',
},
logScroll: {
flex: 1,
},
logEntry: {
fontFamily: 'monospace',
fontSize: 14,
paddingVertical: 6,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
emptyLog: {
color: '#999',
fontStyle: 'italic',
},
});
Exercise 3: Icon Button with hitSlop
Goal: Create small icon buttons with proper touch targets.
Requirements:
- Create an action bar with 4 small icon buttons (emoji icons work fine)
- Visual size should be small (32×32)
- Touch target should be at least 44×44 (use hitSlop)
- Add press feedback (opacity or scale)
- Add accessibility labels
💡 Hint
Calculate hitSlop: if visual is 32px and target should be 44px, you need (44-32)/2 = 6px on each side. Add a bit more for comfort.
✅ Solution
import { Pressable, View, Text, StyleSheet } from 'react-native';
interface ActionButtonProps {
icon: string;
label: string;
onPress: () => void;
}
function ActionButton({ icon, label, onPress }: ActionButtonProps) {
return (
<Pressable
onPress={onPress}
hitSlop={10} // Visual is 32px, this makes touch area ~52px
accessibilityLabel={label}
accessibilityRole="button"
style={({ pressed }) => [
styles.iconButton,
pressed && styles.iconButtonPressed
]}
>
<Text style={styles.icon}>{icon}</Text>
</Pressable>
);
}
export default function ActionBar() {
const handleAction = (action: string) => {
console.log(`${action} pressed`);
};
return (
<View style={styles.container}>
<View style={styles.actionBar}>
<ActionButton
icon="❤️"
label="Like"
onPress={() => handleAction('Like')}
/>
<ActionButton
icon="💬"
label="Comment"
onPress={() => handleAction('Comment')}
/>
<ActionButton
icon="↗️"
label="Share"
onPress={() => handleAction('Share')}
/>
<ActionButton
icon="🔖"
label="Save"
onPress={() => handleAction('Save')}
/>
</View>
<Text style={styles.hint}>
Buttons are 32×32px but touchable area is larger!
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
actionBar: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: 'white',
paddingVertical: 16,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 3,
},
iconButton: {
width: 32,
height: 32,
alignItems: 'center',
justifyContent: 'center',
},
iconButtonPressed: {
opacity: 0.5,
transform: [{ scale: 0.9 }],
},
icon: {
fontSize: 24,
},
hint: {
marginTop: 16,
textAlign: 'center',
color: '#666',
fontSize: 12,
},
});
Exercise 4: Selectable Card List
Goal: Create a list of selectable cards with tap and long-press actions.
Requirements:
- List of cards showing item name and description
- Tap to select/deselect (visual change when selected)
- Long press to show an alert with "Delete?" confirmation
- Selected cards have different background and border
💡 Hint
Use a Set or array to track selected IDs. Use the useRef trick to prevent onPress after onLongPress if needed.
✅ Solution
import { useState, useRef } from 'react';
import { Pressable, View, Text, StyleSheet, Alert, ScrollView } from 'react-native';
interface Item {
id: string;
name: string;
description: string;
}
const ITEMS: Item[] = [
{ id: '1', name: 'Project Alpha', description: 'Mobile app development' },
{ id: '2', name: 'Project Beta', description: 'Backend API design' },
{ id: '3', name: 'Project Gamma', description: 'UI/UX improvements' },
{ id: '4', name: 'Project Delta', description: 'Performance optimization' },
];
export default function SelectableCardList() {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const longPressedRef = useRef(false);
const toggleSelection = (id: string) => {
if (longPressedRef.current) {
longPressedRef.current = false;
return;
}
setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const handleLongPress = (item: Item) => {
longPressedRef.current = true;
Alert.alert(
'Delete Item?',
`Are you sure you want to delete "${item.name}"?`,
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Delete', style: 'destructive', onPress: () => {
console.log(`Deleting ${item.id}`);
}},
]
);
};
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Tap to select, hold to delete</Text>
{ITEMS.map(item => {
const isSelected = selectedIds.has(item.id);
return (
<Pressable
key={item.id}
onPressIn={() => { longPressedRef.current = false; }}
onPress={() => toggleSelection(item.id)}
onLongPress={() => handleLongPress(item)}
delayLongPress={500}
style={({ pressed }) => [
styles.card,
isSelected && styles.cardSelected,
pressed && styles.cardPressed,
]}
>
<View style={styles.cardContent}>
<View style={styles.cardHeader}>
<Text style={[
styles.cardName,
isSelected && styles.cardNameSelected
]}>
{item.name}
</Text>
{isSelected && (
<Text style={styles.checkmark}>✓</Text>
)}
</View>
<Text style={styles.cardDescription}>{item.description}</Text>
</View>
</Pressable>
);
})}
<Text style={styles.selectedCount}>
{selectedIds.size} item{selectedIds.size !== 1 ? 's' : ''} selected
</Text>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
title: {
fontSize: 14,
color: '#666',
marginBottom: 16,
textAlign: 'center',
},
card: {
backgroundColor: 'white',
borderRadius: 12,
marginBottom: 12,
borderWidth: 2,
borderColor: '#e0e0e0',
overflow: 'hidden',
},
cardSelected: {
borderColor: '#2196F3',
backgroundColor: '#e3f2fd',
},
cardPressed: {
opacity: 0.9,
transform: [{ scale: 0.99 }],
},
cardContent: {
padding: 16,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
cardName: {
fontSize: 18,
fontWeight: '600',
color: '#333',
},
cardNameSelected: {
color: '#1565C0',
},
checkmark: {
fontSize: 20,
color: '#2196F3',
fontWeight: 'bold',
},
cardDescription: {
fontSize: 14,
color: '#666',
},
selectedCount: {
textAlign: 'center',
marginTop: 8,
color: '#666',
fontSize: 14,
},
});
Challenge: Custom Button Component Library
🏆 Bonus Challenge
Goal: Create a reusable Button component that can be used throughout an app.
Features:
- Multiple variants: primary, secondary, outline, danger
- Multiple sizes: small, medium, large
- Loading state with ActivityIndicator
- Disabled state
- Optional left or right icon
- Cross-platform feedback (ripple on Android, opacity on iOS)
✅ Solution
import {
Pressable,
Text,
StyleSheet,
ActivityIndicator,
View,
Platform,
ViewStyle,
TextStyle,
} from 'react-native';
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'danger';
type ButtonSize = 'small' | 'medium' | 'large';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
disabled?: boolean;
leftIcon?: string;
rightIcon?: string;
}
const VARIANT_COLORS = {
primary: { bg: '#2196F3', bgPressed: '#1976D2', text: 'white', border: 'transparent' },
secondary: { bg: '#757575', bgPressed: '#616161', text: 'white', border: 'transparent' },
outline: { bg: 'transparent', bgPressed: '#f5f5f5', text: '#2196F3', border: '#2196F3' },
danger: { bg: '#f44336', bgPressed: '#d32f2f', text: 'white', border: 'transparent' },
};
const SIZE_STYLES = {
small: { paddingVertical: 8, paddingHorizontal: 16, fontSize: 14 },
medium: { paddingVertical: 12, paddingHorizontal: 24, fontSize: 16 },
large: { paddingVertical: 16, paddingHorizontal: 32, fontSize: 18 },
};
export default function Button({
title,
onPress,
variant = 'primary',
size = 'medium',
loading = false,
disabled = false,
leftIcon,
rightIcon,
}: ButtonProps) {
const colors = VARIANT_COLORS[variant];
const sizeStyle = SIZE_STYLES[size];
const isDisabled = disabled || loading;
return (
<Pressable
onPress={onPress}
disabled={isDisabled}
android_ripple={{
color: 'rgba(255, 255, 255, 0.3)',
}}
style={({ pressed }) => [
styles.button,
{
backgroundColor: isDisabled
? '#ccc'
: pressed
? colors.bgPressed
: colors.bg,
borderColor: isDisabled ? '#ccc' : colors.border,
paddingVertical: sizeStyle.paddingVertical,
paddingHorizontal: sizeStyle.paddingHorizontal,
opacity: isDisabled && Platform.OS === 'ios' ? 0.6 : 1,
transform: [{
scale: pressed && !isDisabled && Platform.OS === 'ios' ? 0.98 : 1
}],
},
]}
>
<View style={styles.content}>
{loading ? (
<ActivityIndicator
color={colors.text}
size={size === 'small' ? 'small' : 'small'}
/>
) : (
<>
{leftIcon && (
<Text style={[styles.icon, { fontSize: sizeStyle.fontSize }]}>
{leftIcon}
</Text>
)}
<Text style={[
styles.text,
{
color: isDisabled ? '#999' : colors.text,
fontSize: sizeStyle.fontSize,
}
]}>
{title}
</Text>
{rightIcon && (
<Text style={[styles.icon, { fontSize: sizeStyle.fontSize }]}>
{rightIcon}
</Text>
)}
</>
)}
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
borderRadius: 8,
borderWidth: 2,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
content: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontWeight: '600',
},
icon: {
marginHorizontal: 6,
},
});
// Usage examples:
// <Button title="Save" onPress={handleSave} />
// <Button title="Cancel" onPress={handleCancel} variant="secondary" />
// <Button title="Delete" onPress={handleDelete} variant="danger" />
// <Button title="Sign Up" onPress={handleSignUp} variant="outline" size="large" />
// <Button title="Loading..." onPress={() => {}} loading />
// <Button title="Add Item" onPress={handleAdd} leftIcon="+" />
// <Button title="Next" onPress={handleNext} rightIcon="→" />
Summary
🎉 Key Takeaways
- Pressable is the modern standard for handling touch in React Native
- Four press states: onPressIn, onPressOut, onPress, onLongPress
- Style function receives
{ pressed }for dynamic visual feedback - Children function also receives
{ pressed }for dynamic content - hitSlop extends touch area beyond visible bounds for small elements
- pressRetentionOffset controls how far finger can move before cancellation
- android_ripple provides native Android feedback
- Replaces TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, TouchableNativeFeedback
- Always add accessibility labels for screen readers
Quick Reference
// Basic Pressable
<Pressable onPress={handlePress}>
<Text>Press Me</Text>
</Pressable>
// All press callbacks
<Pressable
onPressIn={() => {}} // Finger down
onPressOut={() => {}} // Finger up
onPress={() => {}} // Complete press
onLongPress={() => {}} // Hold 500ms+
delayLongPress={500} // Customize long press delay
>
// Style function for visual feedback
<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.pressed
]}
>
// Children function for dynamic content
<Pressable>
{({ pressed }) => (
<Text>{pressed ? 'Pressing...' : 'Press Me'}</Text>
)}
</Pressable>
// Extended touch area
<Pressable hitSlop={20}>
<Pressable hitSlop={{ top: 10, right: 20, bottom: 10, left: 20 }}>
// Android ripple effect
<Pressable android_ripple={{ color: 'rgba(0,0,0,0.2)' }}>
// Accessibility
<Pressable
accessibilityLabel="Close button"
accessibilityRole="button"
accessibilityHint="Closes the dialog"
>
Common Patterns Cheat Sheet
Opacity Feedback
style={({ pressed }) => ({
opacity: pressed ? 0.7 : 1
})}
Scale Feedback
style={({ pressed }) => ({
transform: [{
scale: pressed ? 0.97 : 1
}]
})}
Color Change
style={({ pressed }) => ({
backgroundColor: pressed
? '#1565C0'
: '#2196F3'
})}
Combined
style={({ pressed }) => [
styles.button,
pressed && styles.pressed
]}
🚀 What's Next?
Now that you can make anything touchable, we'll learn about TextInput — capturing user input with text fields, handling keyboard types, and managing focus in React Native.
👆 Touch Mastered!
You now have complete control over touch interactions in your apps. From simple taps to long presses, from visual feedback to extended touch targets — your buttons will feel polished and professional!