|
| 1 | +--- |
| 2 | +id: touchable |
| 3 | +title: Touchable |
| 4 | +sidebar_label: Touchable |
| 5 | +--- |
| 6 | + |
| 7 | +import HeaderWithBadges from '@site/src/components/HeaderWithBadges'; |
| 8 | + |
| 9 | +:::note |
| 10 | +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](/docs/components/legacy-touchables) section. |
| 11 | +::: |
| 12 | + |
| 13 | +`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. |
| 14 | + |
| 15 | +`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). |
| 16 | + |
| 17 | +If the provided animations are not sufficient, it's possible to use `Touchable` to create fully custom interactions using either [Reanimated](https://docs.swmansion.com/react-native-reanimated/) or [Animated API](https://reactnative.dev/docs/animated). |
| 18 | + |
| 19 | +## Replacing old buttons |
| 20 | + |
| 21 | +If you were using `RectButton` or `BorderlessButton` in your app, you should replace them with `Touchable`. Check out the full code in the [example](#example) section below. |
| 22 | + |
| 23 | +### RectButton |
| 24 | + |
| 25 | +To replace `RectButton` with `Touchable`, simply add `activeUnderlayOpacity={0.105}` to your `Touchable`. This will animate the underlay when the button is pressed. |
| 26 | + |
| 27 | +```tsx |
| 28 | +<Touchable |
| 29 | + ... |
| 30 | + activeUnderlayOpacity={0.105}/> |
| 31 | +``` |
| 32 | + |
| 33 | +### BorderlessButton |
| 34 | + |
| 35 | +Replacing `BorderlessButton` with `Touchable` is as easy as replacing `RectButton`. Just add `activeOpacity={0.3}` to your `Touchable`. This will animate the whole component when the button is pressed. |
| 36 | + |
| 37 | +```tsx |
| 38 | +<Touchable |
| 39 | + ... |
| 40 | + activeOpacity={0.3}/> |
| 41 | +``` |
| 42 | + |
| 43 | +## Migrating from legacy Touchable variants |
| 44 | + |
| 45 | +If you were using the specialized touchable components (`TouchableOpacity`, `TouchableHighlight`, `TouchableWithoutFeedback`, or `TouchableNativeFeedback`), you can replicate their behavior with the unified `Touchable` component. |
| 46 | + |
| 47 | +### TouchableOpacity |
| 48 | + |
| 49 | +To replace `TouchableOpacity`, add `activeOpacity={0.2}`. |
| 50 | + |
| 51 | +```tsx |
| 52 | +<Touchable |
| 53 | + ... |
| 54 | + activeOpacity={0.2}/> |
| 55 | +``` |
| 56 | + |
| 57 | +### TouchableHighlight |
| 58 | + |
| 59 | +To replace `TouchableHighlight`, add `activeUnderlayOpacity={1}`. |
| 60 | + |
| 61 | +```tsx |
| 62 | +<Touchable |
| 63 | + ... |
| 64 | + activeUnderlayOpacity={1}/> |
| 65 | +``` |
| 66 | + |
| 67 | +### TouchableWithoutFeedback |
| 68 | + |
| 69 | +To replace `TouchableWithoutFeedback`, use a plain `Touchable`. |
| 70 | + |
| 71 | +```tsx |
| 72 | +<Touchable ... /> |
| 73 | +``` |
| 74 | + |
| 75 | +### TouchableNativeFeedback |
| 76 | + |
| 77 | +To replicate `TouchableNativeFeedback` behavior, use the [`androidRipple`](#androidripple) prop. Make sure to set `foreground={true}`. |
| 78 | + |
| 79 | +```tsx |
| 80 | +<Touchable |
| 81 | + ... |
| 82 | + androidRipple={{ |
| 83 | + foreground: true, |
| 84 | + }}/> |
| 85 | +``` |
| 86 | + |
| 87 | +## Example |
| 88 | + |
| 89 | +In this example we will demonstrate how to recreate `RectButton` and `BorderlessButton` effects using the `Touchable` component. |
| 90 | + |
| 91 | +<CollapsibleCode |
| 92 | +label="Show full example" |
| 93 | +expandedLabel="Hide full example" |
| 94 | +lineBounds={[7, 40]} |
| 95 | +src={` |
| 96 | +import React from 'react'; |
| 97 | +import { StyleSheet, Text } from 'react-native'; |
| 98 | +import { |
| 99 | + GestureHandlerRootView, |
| 100 | + Touchable, |
| 101 | +} from 'react-native-gesture-handler'; |
| 102 | +
|
| 103 | +export default function TouchableExample() { |
| 104 | + return ( |
| 105 | + <GestureHandlerRootView style={styles.container}> |
| 106 | + <Touchable |
| 107 | + onPress={() => { |
| 108 | + console.log('BaseButton built with Touchable'); |
| 109 | + }} |
| 110 | + style={[styles.button, { backgroundColor: '#7d63d9' }]}> |
| 111 | + <Text style={styles.buttonText}>BaseButton</Text> |
| 112 | + </Touchable> |
| 113 | +
|
| 114 | + <Touchable |
| 115 | + onPress={() => { |
| 116 | + console.log('RectButton built with Touchable'); |
| 117 | + }} |
| 118 | + style={[styles.button, { backgroundColor: '#4f9a84' }]} |
| 119 | + activeUnderlayOpacity={0.105}> |
| 120 | + <Text style={styles.buttonText}>RectButton</Text> |
| 121 | + </Touchable> |
| 122 | +
|
| 123 | + <Touchable |
| 124 | + onPress={() => { |
| 125 | + console.log('BorderlessButton built with Touchable'); |
| 126 | + }} |
| 127 | + style={[styles.button, { backgroundColor: '#5f97c8' }]} |
| 128 | + activeOpacity={0.3}> |
| 129 | + <Text style={styles.buttonText}>BorderlessButton</Text> |
| 130 | + </Touchable> |
| 131 | + </GestureHandlerRootView> |
| 132 | + ); |
| 133 | +} |
| 134 | +
|
| 135 | +const styles = StyleSheet.create({ |
| 136 | + container: { |
| 137 | + flex: 1, |
| 138 | + alignItems: 'center', |
| 139 | + justifyContent: 'center', |
| 140 | +
|
| 141 | + gap: 20, |
| 142 | + }, |
| 143 | + button: { |
| 144 | + width: 200, |
| 145 | + height: 70, |
| 146 | + borderRadius: 15, |
| 147 | + alignItems: 'center', |
| 148 | + justifyContent: 'center', |
| 149 | + }, |
| 150 | + buttonText: { |
| 151 | + color: 'white', |
| 152 | + fontSize: 14, |
| 153 | + fontWeight: '600', |
| 154 | + }, |
| 155 | +}); |
| 156 | +`}/> |
| 157 | + |
| 158 | + |
| 159 | +## Properties |
| 160 | + |
| 161 | +### activeOpacity |
| 162 | + |
| 163 | +```ts |
| 164 | +activeOpacity?: number; |
| 165 | +``` |
| 166 | + |
| 167 | +Defines the opacity of the whole component when the button is active. |
| 168 | + |
| 169 | +### defaultOpacity |
| 170 | + |
| 171 | +```ts |
| 172 | +defaultOpacity?: number; |
| 173 | +``` |
| 174 | + |
| 175 | +Defines the opacity of the whole component when the button is active. By default set to `1`. |
| 176 | + |
| 177 | +### activeUnderlayOpacity |
| 178 | + |
| 179 | +```ts |
| 180 | +activeUnderlayOpacity?: number; |
| 181 | +``` |
| 182 | + |
| 183 | +Defines the opacity of the underlay when the button is active. By default set to `0`. |
| 184 | + |
| 185 | +### defaultUnderlayOpacity |
| 186 | + |
| 187 | +```ts |
| 188 | +defaultUnderlayOpacity?: number; |
| 189 | +``` |
| 190 | + |
| 191 | +Defines the initial opacity of underlay when the button is inactive. By default set to `0`. |
| 192 | + |
| 193 | +### underlayColor |
| 194 | + |
| 195 | +```ts |
| 196 | +underlayColor?: string; |
| 197 | +``` |
| 198 | + |
| 199 | +Background color of the underlay. This only takes effect when `activeUnderlayOpacity` or `defaultUnderlayOpacity` is set. |
| 200 | + |
| 201 | +### exclusive |
| 202 | + |
| 203 | +```ts |
| 204 | +exclusive?: boolean; |
| 205 | +``` |
| 206 | + |
| 207 | +Defines whether pressing this button prevents other buttons exported by Gesture Handler from being pressed. By default set to `true`. |
| 208 | + |
| 209 | +<HeaderWithBadges platforms={['android']}> |
| 210 | +### touchSoundDisabled |
| 211 | +</HeaderWithBadges> |
| 212 | + |
| 213 | +```ts |
| 214 | +touchSoundDisabled?: boolean; |
| 215 | +``` |
| 216 | + |
| 217 | +If set to `true`, the system will not play a sound when the button is pressed. |
| 218 | + |
| 219 | +### onPressIn |
| 220 | + |
| 221 | +<CollapsibleCode |
| 222 | +label="Show composed types definitions" |
| 223 | +expandedLabel="Hide composed types definitions" |
| 224 | +lineBounds={[0, 1]} |
| 225 | +src={` |
| 226 | +onPressIn?: (e: GestureEvent<NativeHandlerData>) => void; |
| 227 | +
|
| 228 | +type GestureEvent<NativeHandlerData> = { |
| 229 | + handlerTag: number; |
| 230 | + numberOfPointers: number; |
| 231 | + pointerType: PointerType; |
| 232 | + pointerInside: boolean; |
| 233 | +} |
| 234 | +
|
| 235 | +enum PointerType { |
| 236 | + TOUCH, |
| 237 | + STYLUS, |
| 238 | + MOUSE, |
| 239 | + KEY, |
| 240 | + OTHER, |
| 241 | +} |
| 242 | +`}/> |
| 243 | + |
| 244 | +Triggered when the button gets pressed (analogous to `onPressIn` in `Pressable` from RN core). |
| 245 | + |
| 246 | +### onPressOut |
| 247 | + |
| 248 | +<CollapsibleCode |
| 249 | +label="Show composed types definitions" |
| 250 | +expandedLabel="Hide composed types definitions" |
| 251 | +lineBounds={[0, 1]} |
| 252 | +src={` |
| 253 | +onPressOut?: (e: GestureEvent<NativeHandlerData>) => void; |
| 254 | +
|
| 255 | +type GestureEvent<NativeHandlerData> = { |
| 256 | + handlerTag: number; |
| 257 | + numberOfPointers: number; |
| 258 | + pointerType: PointerType; |
| 259 | + pointerInside: boolean; |
| 260 | +} |
| 261 | +
|
| 262 | +enum PointerType { |
| 263 | + TOUCH, |
| 264 | + STYLUS, |
| 265 | + MOUSE, |
| 266 | + KEY, |
| 267 | + OTHER, |
| 268 | +} |
| 269 | +`}/> |
| 270 | + |
| 271 | +Triggered when the button gets released or the pointer moves outside of the button area (analogous to `onPressOut` in `Pressable` from RN core). |
| 272 | + |
| 273 | +### onPress |
| 274 | + |
| 275 | +```ts |
| 276 | +onPress?: (pointerInside: boolean) => void; |
| 277 | +``` |
| 278 | + |
| 279 | +Triggered when the button gets pressed (analogous to `onPress` in `Pressable` from RN core). |
| 280 | + |
| 281 | +### onLongPress |
| 282 | + |
| 283 | +```ts |
| 284 | +onLongPress?: () => void; |
| 285 | +``` |
| 286 | + |
| 287 | +Triggered when the button gets pressed for at least [`delayLongPress`](#delaylongpress) milliseconds. |
| 288 | + |
| 289 | + |
| 290 | +### onActiveStateChange |
| 291 | + |
| 292 | +```ts |
| 293 | +onActiveStateChange?: (active: boolean) => void; |
| 294 | +``` |
| 295 | + |
| 296 | +Triggered when the button transitions between active and inactive states. It passes the current active state as a boolean variable to the method as the first parameter. |
| 297 | + |
| 298 | +### delayLongPress |
| 299 | + |
| 300 | +```ts |
| 301 | +delayLongPress?: number; |
| 302 | +``` |
| 303 | + |
| 304 | +Defines the delay, in milliseconds, after which the [`onLongPress`](#onlongpress) callback gets called. By default set to `600`. |
| 305 | + |
| 306 | + |
| 307 | +<HeaderWithBadges platforms={['android']}> |
| 308 | +### androidRipple |
| 309 | +</HeaderWithBadges> |
| 310 | + |
| 311 | +<CollapsibleCode |
| 312 | +label="Show composed types definitions" |
| 313 | +expandedLabel="Hide composed types definitions" |
| 314 | +lineBounds={[0, 1]} |
| 315 | +src={` |
| 316 | +androidRipple?: PressableAndroidRippleConfig; |
| 317 | +
|
| 318 | +type PressableAndroidRippleConfig = { |
| 319 | + color?: (string | OpaqueColorValue); |
| 320 | + borderless?: boolean; |
| 321 | + radius?: number; |
| 322 | + foreground?: boolean; |
| 323 | +} |
| 324 | +`}/> |
| 325 | + |
| 326 | +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. |
0 commit comments