The <Pressable />
component has an unchangeable delay between pressing and visual feedback.
See:
Pressable | TouchableOpacity |
---|---|
![]() |
![]() |
As you can see, the TouchableOpacity
has instant visual feedback when I press it, where as the Pressable takes about 120
ms (wild guess).
I though https://github.com/facebook/react-native/commit/49b2a6e9cd95b74375f51fda87b87430073ec20e should fix this delay?
info Fetching system and libraries information...
(node:46144) Warning: Accessing non-existent property 'padLevels' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
System:
OS: macOS 10.15.5
CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
Memory: 6.86 GB / 32.00 GB
Shell: 5.7.1 - /bin/zsh
Binaries:
Node: 14.4.0 - /usr/local/bin/node
Yarn: 1.22.4 - /usr/local/bin/yarn
npm: 6.14.6 - /usr/local/bin/npm
Watchman: 4.9.0 - /usr/local/bin/watchman
Managers:
CocoaPods: 1.9.3 - /usr/local/bin/pod
SDKs:
iOS SDK:
Platforms: iOS 13.5, DriverKit 19.0, macOS 10.15, tvOS 13.4, watchOS 6.2
Android SDK:
API Levels: 25, 28, 29
Build Tools: 28.0.3, 29.0.2, 29.0.3, 30.0.0, 30.0.0, 30.0.0
System Images: android-29 | Google APIs Intel x86 Atom, android-29 | Google Play Intel x86 Atom
Android NDK: 21.2.6472646
IDEs:
Android Studio: 4.0 AI-193.6911.18.40.6514223
Xcode: 11.5/11E608c - /usr/bin/xcodebuild
Languages:
Java: 1.8.0_252 - /usr/bin/javac
Python: 2.7.16 - /usr/bin/python
npmPackages:
@react-native-community/cli: Not Found
react: 16.13.1 => 16.13.1
react-native: 0.63.1 => 0.63.1
npmGlobalPackages:
*react-native*: Not Found
Provide a detailed list of steps that reproduce the issue.
<Pressable />
componentpressed
argumentThe opacity effect/visual feedback should appear instantly, like on the TouchableOpacity.
PressableOpacity.ts
:
import React, { useCallback } from 'react';
import { Pressable, PressableProps, PressableStateCallbackType, StyleProp, ViewStyle } from 'react-native';
export interface PressableOpacityProps extends PressableProps {
disabledOpacity?: number;
}
export type StyleType = (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
export default function PressableOpacity(props: PressableOpacityProps): JSX.Element {
const { style, disabled, disabledOpacity, ...passThroughProps } = props;
const getOpacity = useCallback(
(pressed: boolean) => {
if (disabled) return disabledOpacity ?? 1;
else return pressed ? 0.6 : 1;
},
[disabled, disabledOpacity],
);
const _style = useCallback<StyleType>(({ pressed }) => [style, { opacity: getOpacity(pressed) }], [getOpacity, style]);
return <Pressable style={_style} disabled={disabled} {...passThroughProps} />;
}
Now just use it:
App.js
export default function App() {
return (<PressableOpacity style={{ width: 200, height: 30 }} onPress={() => console.log('hello!')} />);
}
Still finding this same problem with 0.63.1, and so far the only work around is to monkey patch it to add a default 0 delay.
Note that I'm seeing the odd delay even with TouchableOpacity, not just with Pressables
@cristianoccazinsp How did you patch the delay on the Pressable?
I have only patched the Touchable components since I didn't migrate to Pressable (yet), but I'm seeing the exact same issue, and I would guess that Touchables are using Pressable behind the scenes?
My only work around was to patch the default props like this:
TouchableOpacity.defaultProps = TouchableOpacity.defaultProps || {};
TouchableOpacity.defaultProps.delayPressIn = 0;
TouchableWithoutFeedback.defaultProps = TouchableWithoutFeedback.defaultProps || {};
TouchableWithoutFeedback.defaultProps.delayPressIn = 0;
I am having the same problem. TouchableOpacity is not using Pressable but Pressabilitty API (The same one TouchableOpacity use). That said, Pressable use a hook to use the API, TouchableOpacity is a Class comoponent so it uses the class alternative to use the API. (i don't know if there is any difference between them). Looking to the source code, I can't find a prop or even a reference to a "press in delay". I hope the add such a prop, otherwise Pressable is not usefull for me.
Pressability.js contains the prop. Changing this manually (e.g.: on a TouchableOpacity) will remove the delay, and everything works as expected. On a
I seem to be getting a delay for onPressIn
for all TouchableOpacity
, will verify if also affects TouchableWithoutFeedback
.
Why on earth would you introduce a default delay on this? Surely the whole point of onPressIn
is to be right when someone's finger makes contact with the screen?
I believe the change was made on Pressability API, which all Touchables use. But you can set delayPressIn={0} to fix it.
The problem is with Pressable, cause you can`t change it there.
While not much better than a monkey-patch, you can use patch-package
to change these defaults that should globally to everything affected by this recent change.
We recently upgraded from 61.4
to 63.1
and immediately noticed a significant difference in all of our touch handling (we primarily use `TouchableOpacity).
Looks like this delay option was added here with a sensible default of 0
. It seems like this default may not have been intended, and it was fixed here.
I'm guessing the idea of this delay is to avoid a technically incorrect visual response of a touch on a child touchable during a scroll swipe, since the touch events first goes to the child before being correctly captured by the parent ScrollView
. I agree with this desire, but it seems very difficult to do on the JS side without introducing noticeable delay to nearly all touches, as this thread shows.
Either way it's a significant change from the previous default, unintended or not, that should either by identified on changelog (I checked a few times and didn't see it), or reverted and re-introduced with the change made clear to developers.
Edit: Fixed link to when delay option added
@jehartzog That's the only use case that would make sense, and even then an opt-in would be a way better option. If I actually have the use case of the scroll view, I can manually increase the pressable's delay instead of the other way around.
This seems like a major regression, especially when navigation is involved.
Buttons don't have a chance to highlight before new screens render, making the entire interface feel static and non interactive.
Note that we don't even use Pressable
at all, just react-native-paper
and react-navigation
. Any UI elements with an onPress
that navigate somewhere don't highlight properly since updating to RN 0.63.1. On RN 0.62 it was fine. On react-navigation
the navigation header buttons (such as the < Back
button on iOS), don't visually respond to touches any more unless they are long pressed.
Adding delayPressIn={0}
to react-native-paper
buttons (which use TouchableHighlight
or TouchableNativeFeedback
behind the scenes) fixes it. But this is nonetheless a major regression.
Perhaps @yungsters might know more about this?
Thanks for the tag, @andreialecu.
I'd like to share some history, the purpose of the delay, what changed recently, and what I propose that we do next.
The default "press in" delay of 130ms was always in Touchable.Mixin, but it was accidentally always overridden to zero by TouchableWithoutFeedback, et al.
The purpose of this delay is to prevent accidental activations when a user initiates a scroll gesture. For example, if you have a <ScrollView>
and a <Pressable>
within it, we do not want the <Pressable>
's "pressed" style to be displayed if the user only happened to press it as the start of a scrolling / swiping gesture. The 130ms delay creates enough time for the <ScrollView>
to emit a cancel event if a scroll gesture is detected.
Keep in mind that when a "press out" is detected within the 130ms, we immediately fire onPressIn
and onPressOut
. However, as mentioned above, we still have a problem in which a user who quickly taps an interactive element does not experience any visual feedback. This is because when onPressIn
and onPressOut
fire instantaneously, the onPressIn
visual side effect is not visible. To fix this, we introduced a minimum press duration so that onPressOut
will always fire at least 130ms after onPressIn
(to give people time to see the visual feedback of the element being pressed).
The fix for preserving the default press in duration was only intended for <Pressable>
. But when TouchableWithoutFeedback
(et al) was refactored to use Pressability
, I forgot to inherit the original bug. We can fix that.
There is still the remaining question of what the default behavior for <Pressable>
should be. I recognize the concerns around unnecessary delays, but I also want to highlight the purpose of the delay (to avoid accidental activations on scroll gestures). We could certainly introduce a delayPressIn
prop to <Pressable>
, but I think we should avoid it if possible because I believe the only use case would be setting delayPressIn={0}
.
One thing we could do is change <Pressable>
to default to 130ms only when nested within a <ScrollView>
. The downside of this approach is that if the scrolling container is rendered by a parent view (outside of React Native), <Pressable>
may not know about that and will not behave correctly.
Curious to hear what everyone thinks. Thanks!
@yungsters Thanks for writeup.
There are many cases where having this press in
delay will cause undesired impacts, even if the delay is refactored to intelligently only apply when it detects itself inside a <ScrollView>
. Any CTA style button at the bottom of a ScrollView
which triggers a navigation event will have no animation response to a touch which immediately triggers a navigation event. If the code is well designed, the transitions animations should begin firing well before this 130ms delay is over. There is no way for the framework to know if a developer is using this element to trigger a navigation transition vs just altering some local UI state, so I can't think of any way to just fix this perfectly for everyone.
I do recognize the desire to avoid the undesired press
animation on a touch that ends up captured by the parent ScrollView
, but I don't believe there is a way to do this automatically without making it excessively complicated, and requiring each developer to understand how it's implemented in order to avoid the pitfalls.
My recommendation:
1) Revert back to the pre RN 63 behavior of default delayPressIn={0}
for all Pressability
derived components, including Pressable
.
2) Add the delayPressIn
as an optional prop for Pressable
.
3) Add to the documentation of Pressable
that it's recommended to set delayPressIn={130}
for Pressable
components inside of ScrollViews
, and the reason for doing so. Have a line in there that mentions they may want to avoid this for actions that trigger screen transitions.
Developers who care to actually clean up their UI as much as possible will be able to opt-in to this when needed, and there are no chances of missing the much more important onPress
animation on the pressed component itself. I'd much rather get the extra onPress animation briefly on something I'm scrolling with, than risk pressing on something and getting no visual response.
If you do decide to add the behavior to auto apply delayPressIn={130}
when inside ScrollView, this should be mentioned in docs (and not just source code) so developers are warned that if they fire navigation events from this, they will not get the expected onPress
animation.
One thing we could do is change
to default to 130ms only when nested within a . The downside of this approach is that if the scrolling container is rendered by a parent view (outside of React Native), may not know about that and will not behave correctly.
I'm not sure this would help. In my experience most pressables would be nested inside ScrollViews
.
The most noticeable issue I saw from the added delay in 0.63 was that TouchableHighlights
that were rendered as list items were not changing their background prior to triggering a navigation somewhere else. Since they were already in a FlatList
, this fix wouldn't help in that case and I would still need to set delayPressIn={0}
.
It would also make it inconsistent and I feel users wouldn't understand what's going on.
I have been looking at how other apps do it for a bit, and I noticed that in Outlook for iOS for example, when you tap an email, there's a slight delay (several miliseconds) before it navigates to the screen that shows the email. It's enough time to show visual indication that you tapped on something. And there are no "accidental activations" when scrolling.
Note that I have no experience with Pressable
yet, so I'm writing this from a Touchable*
perspective.
How about something like this:
1) When the user releases the tap, the Touchable*
's visuals are immediately updated to show feedback of the tap.
2) A very small amount of time later (timeout of 10ms? requestAnimationFrame
?), onPress
is called.
Would this not work? (Note that there would need to be a way to prevent repeated taps from triggering onPress
multiple times in quick succession - otherwise it may introduce additional bugs such as navigating twice, etc)
Thanks for the thoughtful (and extremely prompt) responses.
Let me first respond to @andreialecu. Your realization that onPress
is "delayed" in Outlook for iOS begins to touch upon a pretty fundamental disconnect between how React Native (and websites) handle touch feedback for navigation, and how Apple's Human Interface Guidelines handles touch feedback for navigation. It isn't something we can resolve with only changes to Pressable
, but I do believe we need to eventually cross that road. (The gist of the guidelines is that the touch feedback should deactivate after the navigation has been reversed / popped.)
As for next steps, we feel pretty strongly that the delay should be the new default in Pressable
. The reason for this default is that someone who is testing a new interactive user interface is much more likely to notice that the touch feedback is delayed as opposed to testing whether the new interactive element accidentally triggers during a scroll or swipe gesture. But when they realize and want to override that delay, they should be able to do so (or learn why it exists and maybe leave it).
If this makes sense, my proposal is the following:
Pressability
(which affects TouchableWithoutFeedback
, Pressable
, etc.).Pressable
to introduce the 130ms delay when it is enclosed within a ScrollView
.Pressable
to introduce a new prop, pressDelay?: ?('default' | 'none')
. When set to none
, there will be no press delay (regardless of whether it is enclosed within a ScrollView
).As a net effect, TouchableWithoutFeedback
and its composing libraries will continue to work as they did before. However, Pressable
will start with the new behavior (delay by default in ScrollView
) but can be configured otherwise (to have no delay).
Does this seem reasonable?
Would it not work to keep the delay as it is and add an additional delay before onPress
as per my suggestion above? That should theoretically give it enough time to render with the active state before the handler issues the navigation.
@andreialecu We do not actually want to delay the navigation, though. That would make the end user experience worse by actually delaying the navigation and initialization of the next surface.
@yungsters I'm a fan of your proposal, as it is likely to improve future UI without requiring a full audit of existing TouchableWithoutFeedback
uses to ensure that the onPress animation is still working as expected. Pressable
is a far newer addition and unlikely to lead to large amounts of regressions in existing code bases.
@yungsters would it be noticeable though? If it can be made fast enough that onPress
triggers just immediately after the tap is released and the element is highlighted it shouldn't make much difference.
In my experience react native click handlers actually somehow feel too fast compared to other (native) apps when initiating navigation (using react-navigation). In other apps there seems to be an additional delay of several milliseconds (16ms, one frame?) which I associated with them waiting to ensure the feedback is displayed.
@yungsters Here's a possibly better explanation since I was on mobile yesterday:
Scenario A:
1) Tap is detected, the 130 ms timer starts.
2) If tap is released sooner than 130 ms, the active
state renders briefly and immediately (1 frame)
3) onPress
is called, and the app can trigger the navigation.
Scenario B
1) Tap is detected, the 130 ms timer starts.
2) If the tap is held for longer, the 130 ms timer triggers and the active
state renders.
I think scenario A is preferrable for when pressables are nested inside ScrollViews
.
pressDelay: "default"
can be kept to add the extra 1 frame delay between step 2 and 3 in Scenario A. pressDelay: "none"
would keep current RN63 behavior of no delay (but possibly no feedback displayed).
Since Scenario A should only add a few extra frames of delay (out of 60fps), I think it can be defaulted for existing Touchables
as well, and fix some of the issues around them as well. The accidental activation on scroll/swipe would be nice to fix once and for all.
Wouldn't this be much better for UX?
@andreialecu not everyone wants an additional delay for Pressables. In some (most) cases you want the onPress
event to fire immediately, if you do need extra delay, wait those 10ms
using a setTimeout in your onPress
handler.
@mrousavy that won't work in most cases unfortunately. There are thousands of pre-existing react native libraries using Touchables
. They can't be all be reasonably updated to add the delay.
The delay I'm proposing should be absolutely non-noticeable and would make react-native touchables behave more like "native-native" touchables. To keep it inline with @yungsters's proposal, the delay could be "smart" and only be applied when nested in a ScrollView
. And still be disabled with pressDelay: "none"
if necessary.
Additionally, someone could handle onPressIn
instead, which would have no delay. (useful for games, or other situations where fast taps are necessary)
Delaying onPress
is out of the scope for the current issue because we have a path forward. I think that if we want to make that part of React Native feel more like the platform, we should do it for real (and actually hook it up to navigations).
Apologies but I don't understand. I feel we may not be talking about the same issue. I tried rereading this:
(The gist of the guidelines is that the touch feedback should deactivate after the navigation has been reversed / popped.)
I'm not sure what this has to do with anything. By reversing/popping the navigation I understand going back to a previous screen.
The issue at hand is that buttons don't get a chance to update to active/touched state before navigating to a new screen ("pushing" a screen) or before going back ("pulling"/"popping").
The issue here is not isolated to navigation though. I noticed it in a lot of other components that are unrelated to navigation, and which need to be nested in ScrollViews.
With your proposed approach there's still no way to have Touchables behave properly within ScrollViews. They would still show no feedback unless delayPressIn={0}
is specified manually. But in that case scroll or swipe gestures would still temporarily show the button as activated.
What am I missing?
(As a side note, this isn't limited to iOS. The same problem exists on Android)
GIF to demonstrate the problem:
New delay as per RN 63:
Using .patch by @jehartzog above to revert the 130ms delay back to 0:
This is a "Drawer" type screen which is not necessarily navigation. (Arguably it's not in a ScrollView here.)
we should do it for real (and actually hook it up to navigations).
But consider a "call to action" button as part of a ScrollView below, maybe "Open Details" -> which also opens this side drawer, but it's not navigation, just an animated overlay. Would hooking it up to navigations make any difference and automatically handle this?
Here's an example from Android System Settings:
It looks like the navigation is delayed for a little bit after the tap. The ripple even gets a chance to complete. In my experience react-native triggers onPress
so fast that even with a 0
delay, the navigation occurs instantly and you barely can see feedback on what you touched. This is especially visible at the bottom of the screen.
I don't know how, but when I add onPress handler to Pressable. dealy suddenly gone
I feel like compensating for being inside a scrollable container is the more unusual case (I checked several RN codebases I maintain and it was something like 5:1), which makes me think the default behaviour should be no delay, with a prop that can be used to add a delay if required. Maybe something like pressDelay: number | 'scroll' = 0
.
I can confirm that adding an onPress
handler to Pressable
as suggested by @EhsanSarshar would eliminate the delay to the UI feedback (i.e. Android ripple in my case) for me too.
However I believe this is probably unintentional and it would be great if the root cause is fixed instead.
FYI, I am using react-native
of v0.63.0
and I am testing my app on an Android tablet (SDK level 28 if it is helpful to know)
I can confirm that the quick workaround suggested by @EhsanSarshar is working for me too (i.e. adding an
onPress
handler toPressable
and the delay to the UI feedback, i.e. Android ripple in my case, is eliminated)FYI, I am using
react-native
ofv0.63.0
and I am testing my app on an Android tablet (SDK level 28 if it is helpful to know)
That's not a workaround, cause that's not expected behavior. Adding or not an onPress handler should not change the press in delay.
That's not a workaround, cause that's not expected behavior. Adding or not an onPress handler should not change the press in delay.
Yes - I actually meant "quick hack" (to eliminate the delay) instead - editing to avoid confusion.
And I agree that this should be fixed properly instead (say, by exposing an optional prop delayPressIn
as discussed above) and the press in delay should be orthogonal to the onPress
handler
FWIW, sharing how UIKIt handles this: by default, press interactions on buttons are delayed when they're inside of scroll views (and not otherwise). UIScrollView
exposes a delaysContentTouches
property (default on) which controls this behavior. But of course, you don't really want to hard-couple scroll views and buttons; the mechanism is more general. At the UIGestureRecognizer
level, this controls the delaysTouchesBegan
property on the scroll view's UIPanGestureRecognizer
. When that property is true, the gesture recognizer system delays touches targeted on views in the gesture's view subtree.
I have a new project where I used the new Pressable component in several places, but then I started noticing the delay discussed here. After reading through the comments, I've replaced all the Pressable components with TouchableOpacity components with delayPressIn={0}
. Everything is working great now. But I'd love to move back to Pressable (mostly for the hitSlop
prop) once this is cleared up.
Thanks for the thoughtful (and extremely prompt) responses.
Let me first respond to @andreialecu. Your realization that
onPress
is "delayed" in Outlook for iOS begins to touch upon a pretty fundamental disconnect between how React Native (and websites) handle touch feedback for navigation, and how Apple's Human Interface Guidelines handles touch feedback for navigation. It isn't something we can resolve with only changes toPressable
, but I do believe we need to eventually cross that road. (The gist of the guidelines is that the touch feedback should deactivate after the navigation has been reversed / popped.)As for next steps, we feel pretty strongly that the delay should be the new default in
Pressable
. The reason for this default is that someone who is testing a new interactive user interface is much more likely to notice that the touch feedback is delayed as opposed to testing whether the new interactive element accidentally triggers during a scroll or swipe gesture. But when they realize and want to override that delay, they should be able to do so (or learn why it exists and maybe leave it).If this makes sense, my proposal is the following:
- Remove the default 130ms delay from
Pressability
(which affectsTouchableWithoutFeedback
,Pressable
, etc.).- Change
Pressable
to introduce the 130ms delay when it is enclosed within aScrollView
.- Change
Pressable
to introduce a new prop,pressDelay?: ?('default' | 'none')
. When set tonone
, there will be no press delay (regardless of whether it is enclosed within aScrollView
).As a net effect,
TouchableWithoutFeedback
and its composing libraries will continue to work as they did before. However,Pressable
will start with the new behavior (delay by default inScrollView
) but can be configured otherwise (to have no delay).Does this seem reasonable?
Will you add this to a coming release?
Yes, definitely.
Sorry for the radio silence. Let me share my latest plans on this.
In order to avoid the unwanted breaking changes, I am going to rollback the default delay in the upcoming release. In addition, I am adding a new unstable_pressDelay
prop to Pressable
for anyone who — like we do at Facebook — wants to experiment with adding a press delay to prevent unwanted activations on scroll gestures.
There is an awesome component by the name of RectButton inside react-native-gesture-handler which have performant ripple animation in android and highlight animation in ios. I know that it's not related to this thread but maybe it helps some one
After providing delayPressIn={0} to touchable opacity I am still getting the delay in my entire app and it is really a frustrating thing. This delay increases with usage , the more i press buttons delay it is providing to me in next press.
Please suggest a workaround ,need it urgent.
I used the Pressable onTouchStart event to remove the delay for onPressIn Events
Is it resolved or should i downgrade my react native version? Because it is really annoying.
@GuleriaAshish The <Pressable>
component works like a charm for me now.
Fixed in v0.63.3, recommend closing this issue:
Removed default 130ms delay from Pressability and Pressable. (86ffb9c41e by @yungsters)
Most helpful comment
Thanks for the thoughtful (and extremely prompt) responses.
Let me first respond to @andreialecu. Your realization that
onPress
is "delayed" in Outlook for iOS begins to touch upon a pretty fundamental disconnect between how React Native (and websites) handle touch feedback for navigation, and how Apple's Human Interface Guidelines handles touch feedback for navigation. It isn't something we can resolve with only changes toPressable
, but I do believe we need to eventually cross that road. (The gist of the guidelines is that the touch feedback should deactivate after the navigation has been reversed / popped.)As for next steps, we feel pretty strongly that the delay should be the new default in
Pressable
. The reason for this default is that someone who is testing a new interactive user interface is much more likely to notice that the touch feedback is delayed as opposed to testing whether the new interactive element accidentally triggers during a scroll or swipe gesture. But when they realize and want to override that delay, they should be able to do so (or learn why it exists and maybe leave it).If this makes sense, my proposal is the following:
Pressability
(which affectsTouchableWithoutFeedback
,Pressable
, etc.).Pressable
to introduce the 130ms delay when it is enclosed within aScrollView
.Pressable
to introduce a new prop,pressDelay?: ?('default' | 'none')
. When set tonone
, there will be no press delay (regardless of whether it is enclosed within aScrollView
).As a net effect,
TouchableWithoutFeedback
and its composing libraries will continue to work as they did before. However,Pressable
will start with the new behavior (delay by default inScrollView
) but can be configured otherwise (to have no delay).Does this seem reasonable?