Repository navigation
feat(native): paint active and group-active on the UI thread through Reanimated's host pseudo styles - #484
Open
YevheniiKotyrlo wants to merge 3 commits into
Conversation
…king it pressable Registering a container — a size container from `container-name`, or a group a descendant selector names — wired `activeFamily` on the element, and that family's presence is what adds `onPress` and turns a `View` into a `Pressable`. So every container became a touch target: it claimed the JS responder, Android's `JSResponderHandler` then intercepted the touch and cancelled the native gestures of everything beneath it, and the element was announced as clickable and focusable. A container now tracks its `:active` in `containerActiveFamily`, which its queries read and which never adds press handlers. A container already pressed for its own reasons answers through that press; any other observes the touch events its subtree receives, which reach every ancestor without a responder claim.
…ends A container followed one boolean, so with two fingers inside a group, lifting one ended its `:active` while the other still rested there. Each container now follows the touches that went down inside it, and is active until the last of them ends. The touches still on screen prune that set on every event, so a touch that went down elsewhere never keeps it active, and a touch whose end never arrived — its target left the screen first — is dropped at the next touch event.
…Reanimated's host pseudo styles
Contributor
Author
YevheniiKotyrlo
marked this pull request as ready for review
October 15, 2026 13:57
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Problem
active:is tracked on the JS thread: the element becomes a press target, andonPressIn/onPressOutset state that re-renders it with the pressed style, so on a busy JS thread the press feedback lags the finger.group-active:waits the same way, through the container's touch observation.Solution
When Reanimated exposes
setPseudoStyles(software-mansion/react-native-reanimated#10900), a rule whose only press condition is:activeis resolved apart from the resting rules and registered on the element's host view, so the UI thread paints it:active:registers as:active-deepest, the innermost element under the finger, as the JS path behaves today;group-active:registers as:activeon a group the container hosts, so a press anywhere on the container - its padding included - paints the descendant.The pressed style is the element's resolved style with the press rules applied, diffed against the resting one, so variables, specificity and
!importantresolve exactly as they do now, and the press follows the element's own transition. A disabled element registers nothing, and a registration is sent again only when its styles change.The element no longer becomes a
Pressable, so a tap shows the pressed style only while the finger is down, where the JS path held it forPressable's 130 ms minimum.A rule stays on the JS path when it also needs hover or focus, declares variables or containers - except the text-inheritance variables on
Text, whose nested runs inherit natively - or when the installed Reanimated has nosetPseudoStyles, so an app on a released Reanimated behaves exactly as before.Tests
src/__tests__/native/ui-thread-pseudo-classes.test.tsxmocks Reanimated's API and asserts what each element registers: an own press, a group press onText, a resting value React renders, an inline style that outranks the press, a disabled element, the element's transition, an unmount, a rerender that changes nothing, a text colour that stays on the JS path on a view, and a container no element follows.Verification
On Ubuntu with the workflow's steps - build, lint, typecheck,
node --test node/__tests__/*.mjs, the web export, the unstaged-changes checks and commitlint - everything passes, andyarn testpasses 1353 of 1354 tests:Typography - Hyphens › hyphens-nonefailed once in the full run and passes on its own in three runs out of three.On Android release builds - a Galaxy S23 (Android 14, API 34) and an Android 16 (API 36) emulator, with the Reanimated PR above -
active:, andgroup-active:pressed on the container's padding, painted while the press handler blocked the JS thread for 2.5 s and returned to rest on release, while a view beside each, painted from React state set in the sameonPressIn, stayed at rest: in the pressed frames the largest channel change from rest was 208 against 0. The frames are in the evidence comment.Moving the press to the UI thread costs no mount time. On release builds with both PRs, on the Galaxy S23 and a Mi MIX 3 (Android 10), a hundred
Pressables whoseactive:bg-*registers on the UI thread mounted in 49.1 and 133.1 ms, against 49.2 and 142.3 ms for rows whose press stays on the JS path (writtenhover:active:bg-*, which keeps it there) and 49.0 and 130.2 ms for rows with no press style - the median of three runs, each the median of eleven mounts. Re-rendering stayed within the runs' spread, and unmounting took 3.2 and 2.6 ms longer than on the JS path.Not run: the iOS build.
Base
Stacked on #482, whose container touch observation this builds on: the branch is #482's two commits plus one.