Pressable and Touchables
Pressable wraps anything tappable and hands you a pressed flag; the Touchables still work, and Button cannot be styled.
<Pressable onPress={fn} hitSlop={8} style={({ pressed }) => [styles.base, pressed && styles.pressed]} />Pressable is the current primitive for making anything tappable. It wraps its children, detects the phases of a touch and reports them through onPressIn, onPress, onPressOut and onLongPress.
Its style prop accepts either view styles or a function ({ pressed }) => viewStyles. That pressed flag comes from Pressable itself, not from React state. children may be a function taking the same argument. Pressable draws nothing of its own, so if you never read pressed the button looks dead when tapped.
Touch target props
hitSlop={8} is shorthand for hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} and means exactly that. It extends where a press registers without changing layout, which is the fix for a small icon that is hard to hit. pressRetentionOffset controls how far a finger may slide before the press is cancelled. android_ripple turns on the native Android ripple.
Touchables and Button
The older family, TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback and TouchableNativeFeedback, still ships and still works; the docs point to Pressable as the future-proof choice. TouchableOpacity's activeOpacity defaults to 0.2 and it wraps children in an Animated.View, which can shift layout.
Button takes title, onPress, color and disabled, and no style prop. color tints the label on iOS and the background on Android, and the title is uppercased on Android.
Example
import { Pressable, StyleSheet, Text } from "react-native";
type PrimaryButtonProps = {
label: string;
onPress: () => void;
disabled?: boolean;
};
export default function PrimaryButton({
label,
onPress,
disabled = false,
}: PrimaryButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
// Grows the touch area by 8pt on every side without moving anything.
hitSlop={8}
accessibilityRole="button"
// Native ripple on Android; ignored on iOS.
android_ripple={{ color: "#1e3a8a" }}
// style as a function: Pressable passes its own pressed flag in.
// Falsy entries in the array are skipped.
style={({ pressed }) => [
styles.base,
pressed && styles.pressed,
disabled && styles.disabled,
]}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
base: {
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 10,
backgroundColor: "#1d4ed8",
alignItems: "center",
},
pressed: { backgroundColor: "#1e40af" },
disabled: { backgroundColor: "#a1a1aa" },
label: { color: "#ffffff", fontSize: 16, fontWeight: "600" },
});
// On screen: a blue pill with white centred text. Touch down darkens it
// immediately and release runs onPress; on Android a ripple spreads from the
// finger as well. When disabled it turns grey and stops responding.When to use it
- Build a branded button with a visible pressed state and a disabled state.
- Make a small icon reliably tappable without changing the layout around it.
- Read an older screen that still uses TouchableOpacity, and port it.
- Add a long press to a list row for a secondary action.
More examples
TouchableOpacity, and the same thing in Pressable
On screen: two buttons that behave identically, fading to 20% while held. You will meet both in real codebases, so you need to read either. Write new code with Pressable, and note that TouchableOpacity inserts an Animated.View around its children, which can disturb a layout that depends on the exact view tree.
import { Pressable, StyleSheet, Text, TouchableOpacity, View } from "react-native";
type LinkButtonProps = {
label: string;
onPress: () => void;
};
export default function LinkButton({ label, onPress }: LinkButtonProps) {
return (
<View style={styles.row}>
{/* The older form. Fades the children to activeOpacity while held;
0.2 is the default. */}
<TouchableOpacity onPress={onPress} activeOpacity={0.2} style={styles.button}>
<Text style={styles.label}>{label}</Text>
</TouchableOpacity>
{/* The same behaviour with Pressable. The fade is now explicit, which
is the point: nothing is hidden inside the component. */}
<Pressable
onPress={onPress}
style={({ pressed }) => [styles.button, { opacity: pressed ? 0.2 : 1 }]}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
row: { flexDirection: "row", gap: 16, padding: 16 },
button: { paddingVertical: 8, paddingHorizontal: 12 },
label: { color: "#1d4ed8", fontSize: 15, fontWeight: "600" },
});Button: what it does and does not give you
On screen: a platform-default button, blue text on iOS and a blue filled block reading SUBMIT on Android, with a note underneath. Reach for Button while prototyping or in a debug menu. The moment a designer has an opinion about the corner radius, it has to become a Pressable.
import { Button, StyleSheet, Text, View } from "react-native";
type FormFooterProps = {
onSubmit: () => void;
busy: boolean;
};
export default function FormFooter({ onSubmit, busy }: FormFooterProps) {
return (
<View style={styles.footer}>
{/* Button accepts no style prop at all. color is the label colour on
iOS and the background colour on Android, and the title is
uppercased on Android. Height, radius and font are not yours. */}
<Button
title="Submit"
onPress={onSubmit}
disabled={busy}
color="#1d4ed8"
accessibilityLabel="Submit the form"
/>
<Text style={styles.hint}>
Fine for a scratch screen. Anything with a design needs Pressable.
</Text>
</View>
);
}
const styles = StyleSheet.create({
footer: { padding: 16, gap: 8 },
hint: { fontSize: 13, color: "#71717a" },
});An icon button: children as a function, hitSlop written out
On screen: a small close glyph that darkens while held, with a touch area far larger than the glyph itself. Use the children function when the feedback belongs to something inside the button rather than to its container, such as tinting an icon. The hitSlop longhand here is identical to hitSlop={12}.
import { Pressable, StyleSheet, Text } from "react-native";
type CloseButtonProps = {
onClose: () => void;
};
export default function CloseButton({ onClose }: CloseButtonProps) {
return (
<Pressable
onPress={onClose}
onLongPress={() => console.log("long press after 500ms")}
delayLongPress={500} // this is also the default
// Longhand of hitSlop={12}: an 18pt glyph becomes a 42pt target.
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
// How far the finger may wander before the press is cancelled.
pressRetentionOffset={{ top: 20, bottom: 30, left: 20, right: 20 }}
accessibilityRole="button"
accessibilityLabel="Close"
style={styles.hit}
>
{/* children may be a function too, taking the same { pressed }. */}
{({ pressed }) => (
<Text style={[styles.glyph, pressed && styles.glyphPressed]}>X</Text>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
hit: { alignSelf: "flex-start", padding: 4 },
glyph: { fontSize: 18, fontWeight: "700", color: "#52525b" },
glyphPressed: { color: "#18181b" },
});
Discussion