Native Android nested-scroll participation for React Native.
React Native scroll sources already own touch handling, source position, fling physics, and velocity integration. Native Android UI that reacts to the same gesture should participate in that existing transaction, not infer a second one from JavaScript events.
Preserve the physics. Preserve the motion.
Don't synchronize scrolling. Participate in it.
react-native-scroll-interop lets native Android participants join the real synchronous
nested-scroll transaction produced by a supported React Native scroll source. React Native remains
the single owner of source motion.
React Native scroll source
│
▼
real Android nested-scroll transaction
│
▼
PRE → React Native child → POST
│
▼
N native participants
one React Native scroll physics
one synchronous Android nested-scroll transaction
N native participants
Material3 TopAppBar and FloatingToolbar are shipped reference participants used to make the primitive observable. They are not the purpose of the package and are not part of the neutral transport contract.
The clip makes native participation observable through the shipped TopAppBar and FloatingToolbar reference consumers. React Native still owns the gesture, source position, and fling physics. Select the preview for the higher-quality MP4.
react-native-scroll-interop@0.1.0-alpha.1
npm dist-tag: next
This alpha has a deliberately narrow compatibility matrix. Recorded evidence and the current
publication gates are tracked separately in docs/release.md.
The transport has:
- no second source
Scroller/OverScroller; - no parent
scrollBy/scrollToon the React Native source; - no sampled
scrollYtransport; - no timer-based momentum reconstruction;
- no duplicated velocity integration;
- no per-frame JavaScript transport.
These are architecture invariants guarded by the repository checks, not performance slogans.
The library tracks source identity and lifecycle, conserves signed PRE/POST distance, and exposes the same Android transaction to native participants:
requested = preConsumed + childConsumed + postConsumed + remaining
Full contract: docs/architecture.md.
npm install react-native-scroll-interop@nextThe package autolinks as a standard React Native Android package. Expo Modules are not required by the native runtime.
| Target | Current status |
|---|---|
| Expo SDK 57 + React Native 0.86.0 | Recorded PR #26 baseline: exact tarball install, clean prebuild, Android compile/assemble, install/runtime, navigation ownership, touch/fling/reverse-fling |
| bare React Native 0.87.0-rc.3 | Recorded PR #26 baseline: exact tarball install, standard autolinking, compatibility adapter, Android compile/assemble/install, Hermes runtime, touch/fling/reverse-fling |
| React Native 0.87.x | Accepted by the peer range; the repository example builds, but stable 0.87 runtime certification is not yet recorded |
| react-native-screens 4.26.x | Version-scoped navigation ownership adapter validated on the recorded baseline |
| Android | Neutral nested-scroll core + generic RN boundary + shipped Material3 reference participants |
| iOS / web | NativeScrollHost preserves normal View layout, Material reference chrome renders no UI, and navigation options pass through; Android nested-scroll semantics are not emulated |
Peer contracts:
react-native >=0.86.0 <0.87.0 || >=0.87.0-rc.3 <0.88.0
react >=19.2.0 <20.0.0
react-native-safe-area-context >=5.0.0 <6.0.0
react-native-screens >=4.26.0 <4.27.0 optional
expo-router >=57.0.0 <58.0.0 optional
@react-navigation/native-stack >=7.0.0 <8.0.0 optional
The recorded baseline is evidence for those exact consumer shapes, not a blanket certification of
every version admitted by the peer ranges. See docs/release.md.
The current Android boundary recognizes React Native's ReactScrollView and generated
ReactNestedScrollView. Recorded runtime validation uses ScrollView-based Expo and bare examples.
This alpha does not claim verified support for FlatList, FlashList, LegendList, or arbitrary virtualized-list implementations. The architecture can admit additional React Native sources through compatible native source boundaries, but each boundary still requires explicit validation.
Repository-only consumer apps live under examples/:
examples/expo— Expo SDK 57 / React Native 0.86 app using the config plugin and Expo Router integration.examples/bare— bare React Native 0.87 app using standard autolinking, the bare compatibility adapter,NativeScrollHost, andMaterialTopAppBar.
The Expo app is the visual reference demo: Material3 TopAppBar and FloatingToolbar make native
participation visible, but they do not define the purpose of the package. The stable RN 0.87 example
keeps the integration path reproducible; the formal release certification above remains the
recorded 0.87.0-rc.3 gate until the stable line is rerun and documented. Neither example nor any
demo media is shipped in the npm tarball.
The root export contains the ownership host plus the currently shipped Material3 reference participants. It is not intended as a general Material3 component surface.
import {
MaterialToolbar,
MaterialTopAppBar,
NativeScrollHost,
} from 'react-native-scroll-interop';Use NativeScrollHost when the source is not already owned by a supported native screen/container integration.
import { ScrollView } from 'react-native';
import { NativeScrollHost } from 'react-native-scroll-interop';
<NativeScrollHost style={{ flex: 1 }}>
<ScrollView>{/* content */}</ScrollView>
</NativeScrollHost>It discovers a supported RN vertical source and delegates real Android parent callbacks to the generic RN controller. It does not own source motion.
<MaterialTopAppBar
title="Home"
variant="large"
scrollBehavior="exitUntilCollapsed"
/>Key props:
title: string
visible?: boolean
variant?: 'small' | 'medium' | 'large'
scrollBehavior?: 'none' | 'pinned' | 'enterAlways' | 'exitUntilCollapsed'
navigationIcon?: 'none' | 'back'
navigationAccessibilityLabel?: string
onNavigationPress?: () => void
placement?: 'overlay' | 'header'
themeMode?: 'system' | 'light' | 'dark'
dynamicColor?: booleanThe TopAppBar is a native Material3 PRE/POST consumer. It never moves the RN source directly.
pinned, enterAlways, and exitUntilCollapsed map one-to-one to the Material3 scroll behaviors.
none keeps the bar fixed without attaching Material scroll state; unlike pinned, it does not
track content overlap. Navigation defaults to pinned for small bars and
exitUntilCollapsed for every expandable variant.
MaterialToolbar.Root
MaterialToolbar.Content
MaterialToolbar.LeadingContent
MaterialToolbar.TrailingContent
MaterialToolbar.IconButton
MaterialToolbar.TextButton
MaterialToolbar.Icon
MaterialToolbar.Text
MaterialToolbar.Fab
scrollBehavior="exitAlways" observes child-consumed POST distance. FloatingToolbar consumes zero source motion.
node ./node_modules/react-native-scroll-interop/plugin/bareReactNativeScrollCompat.jsThe compatibility adapter is version-scoped to the validated RN 0.86/0.87 source shapes and fails closed on unsupported shapes. The bare example runs this automatically from its postinstall script.
{
"expo": {
"plugins": [
[
"react-native-scroll-interop",
{
"android": {
"reactNativeScrollCompat": true,
"reactNativeScreensInterop": true
}
}
]
]
}
}reactNativeScrollCompat applies the same version-scoped RN source compatibility path used by bare RN. reactNativeScreensInterop enables the validated screens 4.26.x navigation-first ownership path.
The current adapter keeps the native screen/container as the real nested-scroll ancestor and delegates through ReactNativeScreenNestedScrollBridge. The screen-side integration contains no Material3, Expo Router, or React Navigation behavior.
Upstream-neutral design and migration plan: docs/react-native-screens.md.
Two open upstream changes move responsibilities to the layers that already own them:
- React Native #57972 preserves the AndroidX
TYPE_NON_TOUCHnested-scroll lifecycle for ordinaryReactNestedScrollViewflings while React Native continues to initiate and own the fling. - react-native-screens #4537 exposes a neutral Android nested-scroll delegate seam while screens retains ownership and first priority.
They solve different upstream responsibilities. Neither is a blocker for 0.1.0-alpha.1; the alpha
uses narrow, fail-closed compatibility adapters for its currently validated versions.
import { Stack } from 'react-native-scroll-interop/router';
<Stack>
<Stack.Screen
name="index"
options={{
title: 'Home',
headerLargeTitle: true,
material3: {
topAppBar: { scrollBehavior: 'exitUntilCollapsed' },
},
}}
/>
</Stack>The adapter wraps Expo Router's existing Stack. It does not create navigation state or transport scroll frames.
import {
material3NativeStackNavigatorOptions,
material3NativeStackScreenOptions,
withMaterial3NativeStackOptions,
} from 'react-native-scroll-interop/react-navigation';<Stack.Navigator
screenOptions={material3NativeStackNavigatorOptions({
headerLargeTitle: true,
material3: {
topAppBar: { scrollBehavior: 'exitUntilCollapsed' },
},
})}
>
{/* screens */}
</Stack.Navigator>Expo Router and React Navigation share one internal navigator-neutral mapper and Material3 header renderer. Neither adapter owns nested-scroll transport.
npm run check
npm pack --dry-run
npm publish --dry-run --access public --tag nextThe check suite guards architecture boundaries, nested-scroll ownership/conservation, Material3 boundaries, navigation mapping, RN compatibility transformations, the current screens adapter, repository example layout, and npm package surface.
Native/runtime changes additionally require fresh consumer builds and device/emulator validation. See docs/release.md.
0.1.x-alpha is pre-1.0 and uses the next dist-tag. The stable latest tag should wait for stable RN certification, reproducible regression coverage, a settled navigation ownership path, and real external usage.
MIT.