Touchable
This section refers to new Touchable component, meant to replace both buttons and touchables. If you are looking for documentation for the deprecated touchable components, check out the Legacy Touchables section.
Touchable is a versatile new component introduced in Gesture Handler 3 to supersede previous button implementations. Designed for maximum flexibility, it provides a highly customizable interface for native touch handling while ensuring consistent behavior across platforms.
Touchable provides a simple interface for the common animations like opacity, underlay, and scale, implemented entirely on the platform. On Android, it also exposes the native ripple effect on press (turned off by default).
If the provided animations are not sufficient, it's possible to use Touchable to create fully custom interactions using either Reanimated or Animated API.
Replacing old buttons
If you were using RectButton or BorderlessButton in your app, you should replace them with Touchable. Check out the full code in the example section below.
RectButton
To replace RectButton with Touchable, add underlayColor="black" to your Touchable. This will tint the underlay when the button is pressed. The legacy RectButton switches states instantly with no fade, so also set animationDuration={0} to match its behavior.
<Touchable
...
underlayColor="black"
animationDuration={0}/>
The legacy RectButton shows the native theme ripple on Android by default, while Touchable disables the ripple unless androidRipple is set. To keep the legacy Android feedback, set androidRipple={{}} on Android instead of underlayColor/animationDuration — combining the two would render a ripple and an underlay animation simultaneously. Use Platform.select to apply the right props per platform:
import { Platform } from 'react-native';
<Touchable
{...Platform.select({
android: { androidRipple: {} },
default: { underlayColor: 'black', animationDuration: 0 },
})}
/>
BorderlessButton
Replacing BorderlessButton with Touchable is as easy as replacing RectButton. Add activeOpacity={0.3} to dim the whole component on press, and animationDuration={0} to keep the transition instant — matching the legacy BorderlessButton.
<Touchable
...
activeOpacity={0.3}
animationDuration={0}/>
Same caveat as RectButton: the legacy BorderlessButton shows the native theme ripple on Android. To preserve that, set androidRipple={{ borderless: true }} on Android instead of activeOpacity/animationDuration:
<Touchable
{...Platform.select({
android: { androidRipple: { borderless: true } },
default: { activeOpacity: 0.3, animationDuration: 0 },
})}
/>
Migrating from legacy Touchable variants
If you were using the specialized touchable components (TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, or TouchableNativeFeedback), you can replicate their behavior with the unified Touchable component.
TouchableOpacity
To replace TouchableOpacity, add activeOpacity={0.2}. The legacy TouchableOpacity dims instantly on press-in and fades back over 150ms on release, so set animationDuration={{ in: 0, out: 150 }} to mirror that timing.
<Touchable
...
activeOpacity={0.2}
animationDuration={{ in: 0, out: 150 }}/>
TouchableHighlight
A perfect 1:1 replacement isn't possible — in TouchableHighlight the container's own background becomes the underlay (solid underlayColor) and activeOpacity dims just the children on top, so the underlay shows through the dimmed children. Touchable instead has a separate underlay layer between the background and children, and its activeOpacity dims the whole component (background + underlay + children together). The closest approximation: carry underlayColor and activeOpacity over unchanged and add activeUnderlayOpacity={1} so the underlay layer is rendered solid.
<Touchable
...
underlayColor="#DDDDDD"
activeUnderlayOpacity={1}
activeOpacity={0.6}/>
TouchableWithoutFeedback
To replace TouchableWithoutFeedback, use a plain Touchable.
<Touchable ... />
TouchableNativeFeedback
To replicate TouchableNativeFeedback behavior, use the androidRipple prop. The legacy component defaults to useForeground: true, so set foreground: true to match — drop it only if the original code passed useForeground={false}. Add color, radius, or borderless if the original code customized the background prop.
<Touchable
...
androidRipple={{ foreground: true }}/>
Example
In this example we will demonstrate how to recreate RectButton and BorderlessButton effects using the Touchable component.
export default function TouchableExample() {
return (
<GestureHandlerRootView style={styles.container}>
<Touchable
onPress={() => {
console.log('BaseButton built with Touchable');
}}
style={[styles.button, { backgroundColor: '#7d63d9' }]}>
<Text style={styles.buttonText}>BaseButton</Text>
</Touchable>
<Touchable
onPress={() => {
console.log('RectButton built with Touchable');
}}
style={[styles.button, { backgroundColor: '#4f9a84' }]}
underlayColor="black"
animationDuration={0}>
<Text style={styles.buttonText}>RectButton</Text>
</Touchable>
<Touchable
onPress={() => {
console.log('BorderlessButton built with Touchable');
}}
style={[styles.button, { backgroundColor: '#5f97c8' }]}
activeOpacity={0.3}
animationDuration={0}>
<Text style={styles.buttonText}>BorderlessButton</Text>
</Touchable>
</GestureHandlerRootView>
);
}
Properties
activeOpacity
activeOpacity?: number;
Defines the opacity of the whole component when the button is active.
defaultOpacity
defaultOpacity?: number;
Defines the opacity of the whole component when the button is active. By default set to 1.
activeScale
activeScale?: number;
Defines the scale of the whole component when the button is active.
defaultScale
defaultScale?: number;
Defines the scale of the whole component when the button is inactive. By default set to 1.
activeUnderlayOpacity
activeUnderlayOpacity?: number;
Defines the opacity of the underlay when the button is active. By default set to 0.105.
defaultUnderlayOpacity
defaultUnderlayOpacity?: number;
Defines the initial opacity of underlay when the button is inactive. By default set to 0.
hoverOpacity
hoverOpacity?: number;
Defines the opacity of the whole component when the button is hovered. By default falls back to defaultOpacity.
hoverScale
hoverScale?: number;
Defines the scale of the whole component when the button is hovered. By default falls back to defaultScale.
hoverUnderlayOpacity
hoverUnderlayOpacity?: number;
Defines the opacity of the underlay when the button is hovered. By default falls back to defaultUnderlayOpacity.
animationDuration
animationDuration?: AnimationDuration;
Press and hover animation timing, in milliseconds. Defaults to 50ms for the in phase and 100ms for the out phase.
Each animation has two phases — in (running while the pointer engages the component) and out (running after the pointer releases) — across two categories:
tap— applies to presses.hover— pointer hover (web only).
longPress is an optional override for the press-out timing once the press has been held past delayLongPress. It only has an out field (the press-in is always the tap in duration). If omitted, the long-press release uses the resolved tap out duration.
Three input shapes are accepted:
- A single number applied to every phase of every category:
<Touchable animationDuration={200} />
- A baseline
in/outwith optional per-category overrides — categories that aren't specified inherit the baseline, and within a category any field left out also inherits from the baseline:
<Touchable
animationDuration={{
in: 50,
out: 200,
hover: { in: 400 },
}}
/>
- Both categories specified explicitly (no baseline) — every field must be supplied:
<Touchable
animationDuration={{
tap: { in: 0, out: 200 },
hover: { in: 300, out: 300 },
}}
/>
To give a long press its own release timing, add longPress.out. The switch fires when the press is held past delayLongPress:
<Touchable
delayLongPress={400}
onLongPress={() => {}}
animationDuration={{
in: 50,
out: 100,
longPress: { out: 500 },
}}
/>
underlayColor
underlayColor?: string;
Background color of the underlay. This only takes effect when activeUnderlayOpacity or defaultUnderlayOpacity is set. By default set to transparent.
exclusive
exclusive?: boolean;
Defines whether pressing this button prevents other buttons exported by Gesture Handler from being pressed. By default set to true.
touchSoundDisabled
touchSoundDisabled?: boolean;
If set to true, the system will not play a sound when the button is pressed.
onPressIn
onPressIn?: (e: GestureEvent<NativeHandlerData>) => void;
Triggered when the button gets pressed (analogous to onPressIn in Pressable from RN core).
onPressOut
onPressOut?: (e: GestureEvent<NativeHandlerData>) => void;
Triggered when the button gets released or the pointer moves outside of the button area (analogous to onPressOut in Pressable from RN core).
onPress
onPress?: (e: GestureEvent<NativeHandlerData>) => void;
Triggered when the button gets pressed (analogous to onPress in Pressable from RN core).
onLongPress
onLongPress?: () => void;
Triggered when the button gets pressed for at least delayLongPress milliseconds.
delayLongPress
delayLongPress?: number;
Defines the delay, in milliseconds, after which the onLongPress callback gets called. By default set to 600.
androidRipple
androidRipple?: PressableAndroidRippleConfig;
Configuration for the ripple effect on Android. If not provided, the ripple effect will be disabled. If {} is provided, the ripple effect will be enabled with default configuration.
cancelOnLeave?: boolean;
Whether the touch should be canceled when the pointer leaves the component. By default set to true. On web this prop doesn't have any effect and behaves as if true was set.
needsOffscreenAlphaCompositing
needsOffscreenAlphaCompositing?: boolean;
Whether the view should render with an offscreen alpha-compositing buffer when its opacity is less than 1. Defaults to false.