A lightweight, production-ready toolkit for smooth, scroll-driven UI with Reanimated 3.
- UI-thread animations at 60 FPS
- Automatic direction detection with threshold
- Simple context + hooks API
- Ready-made components: motionify views and bottom tab
- Typed TypeScript API
motionify-demo.mp4
# npm
npm install react-native-motionify
# yarn
yarn add react-native-motionify
# pnpm
pnpm add react-native-motionify
# bun
bun add react-native-motionify
# peer deps
npm install react-native-reanimated@^3.0.0Follow Reanimated 3 setup: https://docs.swmansion.com/react-native-reanimated/docs/3.x/fundamentals/getting-started.
import { MotionifyProvider } from "react-native-motionify";
export default function App() {
return (
<MotionifyProvider threshold={8} supportIdle={false}>
<YourApp />
</MotionifyProvider>
);
}import { ScrollView, Text, View } from "react-native";
import { useMotionify } from "react-native-motionify";
function Screen() {
const { onScroll, direction } = useMotionify();
return (
<ScrollView onScroll={onScroll} scrollEventThrottle={16}>
<Text>Direction: {direction}</Text>
<View style={{ height: 2000 }} />
</ScrollView>
);
}import { MotionifyBottomTab } from "react-native-motionify";
function AppShell() {
return (
<>
<Screen />
<MotionifyBottomTab hideOn="down" translateRange={{ from: 0, to: 80 }}>
<TabBar />
</MotionifyBottomTab>
</>
);
}Any screen that participates in motionify behavior must attach the onScroll from useMotionify() to its ScrollView/FlatList/SectionList and set scrollEventThrottle={16}.
// ScrollView example
const { onScroll } = useMotionify();
<ScrollView onScroll={onScroll} scrollEventThrottle={16} />;// FlatList example
const { onScroll } = useMotionify();
<FlatList
data={items}
keyExtractor={(it) => it.id}
renderItem={renderItem}
onScroll={onScroll}
scrollEventThrottle={16}
/>;// FlashList (shopify/flash-list) example
import { FlashList } from "@shopify/flash-list";
const { onScroll } = useMotionify();
<FlashList
data={items}
renderItem={renderItem}
estimatedItemSize={72}
onScroll={onScroll}
scrollEventThrottle={16}
/>;// LegendList example
import { LegendList } from "legendapp-ui";
const { onScroll } = useMotionify();
<LegendList
data={items}
renderItem={renderItem}
onScroll={onScroll}
scrollEventThrottle={16}
/>;Use this when you only need views to react to scroll (e.g., headers, FABs, content blocks).
import { ScrollView } from "react-native";
import {
MotionifyProvider,
useMotionify,
MotionifyView,
} from "react-native-motionify";
function Screen() {
const { onScroll } = useMotionify();
return (
<ScrollView onScroll={onScroll} scrollEventThrottle={16}>
{/* content */}
<MotionifyView
animatedY
hideOn="down"
translateRange={{ from: 0, to: 60 }}
>
<FAB />
</MotionifyView>
</ScrollView>
);
}
export default function App() {
return (
<MotionifyProvider>
<Screen />
</MotionifyProvider>
);
}Note: Attach onScroll only once per scrollable container. Child motionify components consume context automatically.
Use this when you want a bottom tab to hide/show with scroll.
import {
MotionifyProvider,
MotionifyBottomTab,
useMotionify,
} from "react-native-motionify";
function Screen() {
const { onScroll } = useMotionify();
return (
<ScrollView onScroll={onScroll} scrollEventThrottle={16}>
{/* content */}
</ScrollView>
);
}
function AppShell() {
return (
<>
<Screen />
<MotionifyBottomTab hideOn="down" translateRange={{ from: 0, to: 80 }}>
<TabBar />
</MotionifyBottomTab>
</>
);
}
export default function App() {
return (
<MotionifyProvider>
<AppShell />
</MotionifyProvider>
);
}Notes:
- Ensure each screen that should control the tab wires
onScroll. - Use
excludeandcurrentIdonMotionifyBottomTabto keep the tab visible on specific routes.
<MotionifyProvider threshold={8} supportIdle={false}>
- threshold: number — pixels to switch direction
- supportIdle: boolean — emit
idleafter inactivity
useMotionify(config?)
Returns:
- scrollY: SharedValue
- direction: 'up' | 'down' | 'idle'
- directionShared: SharedValue<'up' | 'down' | 'idle'>
- isScrolling: boolean
- onScroll: Scroll handler for
ScrollView/FlatList - setThreshold(threshold)
- setSupportIdle(enabled)
Optional config: { threshold?: number; supportIdle?: boolean }
-
<MotionifyView>- Quick direction-based animations
- Props:
animatedY?,fadeScale?,customEffects?,hideOn='down'|'up',translateRange={from,to},animationDuration,supportIdle,easing
-
<MotionifyViewWithInterpolation>- Interpolate styles from scroll position
- Props:
interpolations,value?,customAnimatedStyle?
-
<MotionifyBottomTab>- Hide/show on scroll
- Props:
hideOn,translateRange,animationDuration,supportIdle,exclude?,currentId?
-
<MotionifyBottomTabWithInterpolation>- Smooth, range-based translation
- Props:
inputRange,outputRange,extrapolate,scrollValue?
DEFAULTS: threshold, durations, idle timeout, throttleTRANSLATION_PRESETS: common ranges (e.g.,BOTTOM_TAB,FAB_*,HEADER)INTERPOLATION_PRESETS: fade/scale/parallax/rotate/sticky presets- Helpers:
createInterpolation,createFadeInterpolation,createScaleInterpolation,createParallaxInterpolation,createRotationInterpolation,clamp,lerp,mapRange
<MotionifyProvider>
<Screen />
<MotionifyBottomTab hideOn="down" translateRange={{ from: 0, to: 80 }}>
<TabBar />
</MotionifyBottomTab>
</MotionifyProvider><MotionifyViewWithInterpolation
interpolations={{
translateY: {
inputRange: [0, 200],
outputRange: [0, -100],
extrapolate: "extend",
},
opacity: { inputRange: [0, 150, 200], outputRange: [1, 0.5, 0] },
}}
>
<Image source={headerImage} />
</MotionifyViewWithInterpolation>const { onScroll } = useMotionify({ threshold: 20 });
<MotionifyView fadeScale hideOn="down" animationDuration={400}>
<FAB />
</MotionifyView>;You can skip MotionifyView/MotionifyBottomTab and drive your own Animated.* components using values from the hook.
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolation,
} from "react-native-reanimated";
import { useMotionify } from "react-native-motionify";
function CustomScreen() {
const { onScroll, scrollY, directionShared } = useMotionify();
const animatedHeaderStyle = useAnimatedStyle(() => {
const translateY = interpolate(
scrollY.value,
[0, 200],
[0, -100],
Extrapolation.CLAMP
);
const opacity = directionShared.value === "down" ? 0.7 : 1;
return { transform: [{ translateY }], opacity };
});
return (
<Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}>
<Animated.View style={animatedHeaderStyle}>
<Header />
</Animated.View>
<Content />
</Animated.ScrollView>
);
}Notes:
- Use
scrollYand/ordirectionSharedto derive your own animations. - Works with
Animated.ScrollView,Animated.FlatList, or anyAnimated.View. - Keep worklets light; precompute heavy values outside.
- Wrap your app with
MotionifyProvideronce. - In each scrollable screen, call
useMotionify()and wireonScroll+scrollEventThrottle={16}. - Choose either:
- Normal screens: use
MotionifyViewfor direction-based effects. - Bottom tabs: use
MotionifyBottomTab(optionally withexclude/currentId). - Fully custom: use
scrollY/directionSharedwith Reanimated styles.
- Normal screens: use
- Use
scrollEventThrottle={16} - Keep worklets light; precompute heavy values
- Prefer interpolation for smoother motion
- Use
LegendList,FlashListorFlatListfor long content
To contribute to this library:
- Make changes to the implementation in
react-native-motionify. - Test with various build configurations.
- Submit pull requests with clear descriptions of changes and benefits.
MIT