React Native Header Motion: Headless Plumbing for Headers That Actually Collapse
The Gray Cat
Animated, collapsible headers are deceptively hard. The demo version is a few lines: wire a scroll handler to a shared value, interpolate a translateY, ship it. Then the real app arrives. The header height depends on safe-area insets and dynamic content you can't know ahead of time. There are three scroll views under a tab bar that all need to stay in sync. And the header itself is rendered by React Navigation in a sibling tree that can't see any of your screen's state. React Native Header Motion is built for exactly that gap. It is a low-level, headless toolkit that orchestrates scroll-driven header motion on top of Reanimated, deliberately shipping plumbing rather than UI.
The mental model is simple: the library measures your header, derives a normalized progress value that runs from 0 (fully expanded) to 1 (fully collapsed) as you scroll, and exposes that value as a Reanimated shared value. You author every visual yourself with standard useAnimatedStyle interpolation. What you don't have to author is the measurement, the multi-scrollable coordination, and the navigation boundary crossing. That last one is the library's defining feature.
Where It Fits in the Ecosystem
It helps to place this library on a spectrum. At the high-level end you have batteries-included options like react-native-collapsible-tab-view or @codeherence/react-native-header, which hand you styled Header and LargeHeader components and an opinionated layout. They get you to a polished result fast, but you adopt their component model. At the low-level end you have raw Reanimated, where you get nothing but a blank canvas and a useAnimatedScrollHandler.
React Native Header Motion sits in between. It is the headless layer: it gives you the three things you'd inevitably reinvent (runtime measurement of the collapsible region, synchronization across multiple scrollables, and the navigation-boundary bridge) while leaving every pixel of styling to you. If you only have one simple screen, raw Reanimated is fine. The moment you hit dynamic heights, shared headers across tabs, or a navigation-managed header, this library earns its place.
What You Get in the Box
- Scroll-driven progress. A normalized
progressshared value (0 expanded, 1 collapsed) you interpolate however you like. - Automatic runtime measurement. Mark the collapsible region and the library measures it, deriving the scroll distance that maps to
progress = 1. No magic height numbers. - Multi-scrollable sync. Tab views and pagers keep a shared header from jumping when you switch between scroll views.
- Explicit navigation bridging. A
Bridge/NavigationBridgepair carries motion state into headers rendered by Expo Router or React Navigation. - Drop-in scrollables.
ScrollViewandFlatListequivalents, plus a factory to wrap third-party lists like FlashList and LegendList. - Optional header panning. Let users drag the header itself to expand or collapse it, powered by Gesture Handler.
Getting It Installed
Install the package with your manager of choice:
npm install react-native-header-motion
yarn add react-native-header-motion
This is where you should pause and check your stack. React Native Header Motion v1 has a hard requirement on Reanimated v4 and the separate react-native-worklets package that Reanimated 4 split out, along with Gesture Handler v2. Its peer dependencies look like this:
{
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-gesture-handler": "^2.0.0",
"react-native-reanimated": "^4.0.0",
"react-native-worklets": ">=0.4.0"
}
}
If your app is still on Reanimated v3, you cannot use this library without upgrading first. After installing, complete the standard native setup for Reanimated, Worklets, and Gesture Handler (the Babel plugin and the rest). The library ships ESM modules and TypeScript types, and its development matrix pins recent versions: RN 0.83, React 19.2, Reanimated 4.2.1, and Gesture Handler 2.30.
Your First Collapsing Header
The default export, HeaderMotion, is a provider with compound components hanging off it. The provider measures your header and owns the shared motion values. Your header reads those values through the useMotionProgress() hook. Here is the canonical pattern, drawn from the library's own example app: a header that translates up as you scroll, with a title that counter-translates to stay pinned, and a dynamic region that fades and scales away.
import HeaderMotion, { useMotionProgress } from 'react-native-header-motion';
import Animated, {
Extrapolation,
interpolate,
useAnimatedStyle,
} from 'react-native-reanimated';
import { View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function CollapsibleHeader() {
const { progress, progressThreshold } = useMotionProgress();
const insets = useSafeAreaInsets();
const containerStyle = useAnimatedStyle(() => {
const threshold = progressThreshold.get();
const translateY = interpolate(
progress.get(),
[0, 1],
[0, -threshold],
Extrapolation.CLAMP
);
return { transform: [{ translateY }] };
});
const titleStyle = useAnimatedStyle(() => {
const threshold = progressThreshold.get();
const translateY = interpolate(
progress.get(),
[0, 1],
[0, threshold],
Extrapolation.CLAMP
);
return { transform: [{ translateY }] };
});
const boxSectionStyle = useAnimatedStyle(() => {
const opacity = interpolate(progress.get(), [0, 0.6], [1, 0], Extrapolation.CLAMP);
const scale = interpolate(progress.get(), [0, 1], [1, 0.8], Extrapolation.CLAMP);
return { opacity, transform: [{ scale }] };
});
return (
<HeaderMotion.Header style={[{ paddingTop: insets.top }, containerStyle]}>
<Animated.View style={titleStyle}>
<TitleWithSubtitle title="Title" subtitle="Subtitle" />
</Animated.View>
<View style={{ overflow: 'hidden' }}>
<HeaderMotion.Header.Dynamic style={boxSectionStyle}>
<DynamicBox />
<DynamicBox />
</HeaderMotion.Header.Dynamic>
</View>
</HeaderMotion.Header>
);
}
The key concept here is Header.Dynamic. The region you wrap in it is the collapsible part, and its measured height is what defines the collapse distance, exposed to you as progressThreshold. The sticky region (the title) lives outside Header.Dynamic and counter-translates by the same threshold to stay pinned at the top. Everything inside useAnimatedStyle runs on the UI thread, which is why you read shared values with .get() inside the worklet.
Wiring the Screen Together
A header alone does nothing. You need a scrollable that reports its position to the provider. The library gives you drop-in replacements, so the simplest screen is just the provider, your header, and a HeaderMotion.ScrollView:
import HeaderMotion from 'react-native-header-motion';
import { Text } from 'react-native';
export default function ProfileScreen() {
return (
<HeaderMotion>
<CollapsibleHeader />
<HeaderMotion.ScrollView contentContainerStyle={{ padding: 16 }}>
{longContent.map((item) => (
<Text key={item.id}>{item.body}</Text>
))}
</HeaderMotion.ScrollView>
</HeaderMotion>
);
}
That's the entire single-screen story. The provider measures the header, the ScrollView feeds it scroll offsets, and useMotionProgress() inside your header reacts. If you'd rather render a list, swap in HeaderMotion.FlatList, which takes the same props as React Native's FlatList.
Crossing the Navigation Boundary
Here is the problem that justifies the whole library. When you use Expo Router or React Navigation, the header is rendered in a sibling React tree, outside your screen. A context or shared value you create inside your screen simply isn't visible to that header component. Most libraries paper over this; React Native Header Motion treats it as a first-class concern and gives you an explicit two-piece bridge.
HeaderMotion.Bridge is a render-prop that yields the live context value. You hand that value to your navigation header's options, and inside the header you wrap your component in HeaderMotion.NavigationBridge to re-provide it. Now useMotionProgress() works inside the navigation-rendered header.
import HeaderMotion from 'react-native-header-motion';
import { Stack } from 'expo-router';
export default function Screen() {
return (
<HeaderMotion>
<HeaderMotion.Bridge>
{(value) => (
<Stack.Screen
options={{
header: () => (
<HeaderMotion.NavigationBridge value={value}>
<CollapsibleHeader />
</HeaderMotion.NavigationBridge>
),
}}
/>
)}
</HeaderMotion.Bridge>
<HeaderMotion.ScrollView>{content}</HeaderMotion.ScrollView>
</HeaderMotion>
);
}
The explicitness is the point. Because the boundary is named rather than hidden, the same pattern works with bare React Navigation or any header renderer that accepts a header: () => ... option. You always know exactly where the context is being carried across the tree.
Sharing One Header Across Tabs and Pagers
The second hard problem the library handles is multiple scrollables under one header. A profile screen with Posts, Media, and Likes tabs has three independent scroll views, and a naive setup makes the header jump every time you switch tabs because each scrollable has its own offset. HeaderMotion.ScrollManager coordinates them, and the activeScrollId shared value tells the manager which scrollable is currently driving the header.
import HeaderMotion from 'react-native-header-motion';
import { useSharedValue } from 'react-native-reanimated';
function TabbedProfile() {
const activeScrollId = useSharedValue('posts');
return (
<HeaderMotion activeScrollId={activeScrollId}>
<CollapsibleHeader />
<HeaderMotion.ScrollManager>
<HeaderMotion.FlatList data={posts} renderItem={renderPost} />
<HeaderMotion.FlatList data={media} renderItem={renderMedia} />
<HeaderMotion.FlatList data={likes} renderItem={renderLike} />
</HeaderMotion.ScrollManager>
</HeaderMotion>
);
}
The HeaderMotion root also accepts a few tuning props worth knowing. progressThreshold lets you override the auto-measured collapse distance, either as a fixed number or as a function that transforms the measured dynamic height. measureDynamicMode switches between measuring once on 'mount' or re-measuring on 'update' when the dynamic content changes size. And progressExtrapolation controls how progress behaves beyond the [0, 1] range, defaulting to clamping.
Bringing Your Own List and Letting Users Drag
If you've standardized on a faster list like FlashList or LegendList, you aren't locked out of the drop-in components. createHeaderMotionScrollable() is a factory that turns any scrollable into a Header Motion-aware one, and ScrollablePresets ships ready-made wrappers for common third-party lists.
import { createHeaderMotionScrollable, ScrollablePresets } from 'react-native-header-motion';
import { FlashList } from '@shopify/flash-list';
const HeaderMotionFlashList = createHeaderMotionScrollable(
FlashList,
ScrollablePresets.flashList
);
For an extra touch of polish, the optional HeaderPanBoundary lets users drag the header itself to expand or collapse it, rather than only scrolling. This is the reason Gesture Handler is a required peer dependency even though scroll-only motion wouldn't need it: panning is built on gestures.
One performance note the docs are emphatic about. Animate transforms and opacity, never layout properties like width, height, padding, or margin. Transforms and opacity stay on the UI thread; layout animations bounce to the JS thread and jank. Every interpolation in the examples above sticks to translateY, scale, and opacity for exactly this reason.
A Word on Maturity
Honesty matters when you're choosing a foundation. React Native Header Motion is young and solo-maintained by its author, Oskar Pawica. It first published in early 2026, reached v1.0.0 only weeks before v1.0.1, and has a few hundred GitHub stars at the time of writing. The jump from v0.x to v1.0 was a full rewrite with a migration guide, so the API has already churned once and may still be settling. Being headless by design also means more code on your side: there is no "drop in a nice header" shortcut here.
None of that is a dealbreaker, but it does shape who this is for. If you want a polished header in fifteen minutes, reach for a higher-level library. If you're building something custom, you've outgrown raw Reanimated boilerplate, and you specifically need that navigation-boundary bridge, React Native Header Motion is the cleanest expression of that plumbing available today. It does the unglamorous parts well and gets out of your way for the rest.