Description:
react-native-collapsible-tab is a React Native component that adds a collapsible header to a swipeable tab view, with per-tab scroll memory, jump-free tab switching, and built-in adapters for FlatList, ScrollView, SectionList, FlashList, and LegendList.
The library decouples the header animation from individual tab scroll offsets. That means the header never flickers or jumps when you change tabs, and each tab remembers exactly where you scrolled.
It runs on the UI thread via Reanimated and Gesture Handler, works on old and new architecture, and supports Expo managed workflow.
Features
- Adds collapsible header tabs for React Native screens.
- Keeps scroll position per tab through stable tab names.
- Supports FlatList, ScrollView, SectionList, FlashList v2, and LegendList.
- Runs header and tab animations through Reanimated shared values.
- Supports drag gestures on the header while header buttons remain tappable.
- Adds lazy tab mounting with destination-only mounting for far tab jumps.
- Exposes hooks for header progress, active tab scroll offset, and animated tab index.
- Includes imperative methods for tab navigation and scroll-to-top actions.
Use Cases
- Profile screens need posts, media, likes, and about sections under the same collapsing user header.
- Social feeds benefit from tab-specific scroll memory when people move between “Following,” “For You,” and saved lists.
- Product pages can keep image galleries, reviews, details, and Q&A inside one mobile tab interface.
- Media apps often need a pinned tab bar below artwork, creator details, or a channel header.
- Analytics or admin mobile screens may use separate list tabs while keeping filters or account context visible near the top.
How To Use It
Installation
Install the package with NPM:
npm install react-native-collapsible-tabInstall the required peer dependencies with Expo:
npx expo install react-native-reanimated react-native-gesture-handler react-native-pager-viewAdd optional list backends only when your screen uses those adapters:
npx expo install @shopify/flash-list
npm install @legendapp/listUse these version requirements as the baseline: Reanimated 3.6 or later, Gesture Handler 2 or later, and PagerView 6 or later. FlashList support targets FlashList v2 and requires the New Architecture. Web is not supported because PagerView is native-only. ([GitHub][1])
Your app root must include GestureHandlerRootView. Expo Router handles this root setup, but a plain React Native app usually needs it in App.tsx.
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { ProfileTabs } from './ProfileTabs';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ProfileTabs />
</GestureHandlerRootView>
);
}Basic Usage
import { Text, View } from 'react-native';
import { Tabs } from 'react-native-collapsible-tab';
const articles = [
{ id: 'a1', title: 'Building a mobile feed' },
{ id: 'a2', title: 'Designing tabbed profiles' },
];
function AccountHeader() {
return (
<View style={{ padding: 24, backgroundColor: '#ffffff' }}>
<Text style={{ fontSize: 28, fontWeight: '700' }}>Jane Cooper</Text>
<Text style={{ marginTop: 6 }}>Frontend engineer and product builder</Text>
</View>
);
}
export function ProfileTabs() {
return (
<Tabs.Container renderHeader={() => <AccountHeader />}>
<Tabs.Tab name="articles" label="Articles">
<Tabs.FlatList
data={articles}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Text style={{ padding: 16 }}>{item.title}</Text>
)}
/>
</Tabs.Tab>
<Tabs.Tab name="about" label="About">
<Tabs.ScrollView>
<Text style={{ padding: 16 }}>
Bio, links, location, and account details.
</Text>
</Tabs.ScrollView>
</Tabs.Tab>
</Tabs.Container>
);
}Advanced Examples
Profile Tabs With Lazy Mounting And Snap
Use lazy when secondary tabs have expensive content. Add snapThreshold when the header should settle open or closed after partial scroll gestures.
import { useState } from 'react';
import { Text, View } from 'react-native';
import { Tabs } from 'react-native-collapsible-tab';
type TabEvent = {
prevTabName: string;
tabName: string;
};
function CreatorHeader() {
return (
<View style={{ padding: 20, backgroundColor: '#ffffff' }}>
<Text style={{ fontSize: 26, fontWeight: '700' }}>Design Notes</Text>
<Text>Components, tutorials, and saved references.</Text>
</View>
);
}
export function CreatorScreen() {
const [activeTab, setActiveTab] = useState('posts');
return (
<>
<Text style={{ padding: 12 }}>Active tab: {activeTab}</Text>
<Tabs.Container
renderHeader={() => <CreatorHeader />}
minHeaderHeight={44}
lazy
snapThreshold={0.5}
onTabChange={({ tabName }: TabEvent) => setActiveTab(tabName)}
>
<Tabs.Tab name="posts" label="Posts">
<Tabs.FlatList
data={[1, 2, 3, 4, 5]}
keyExtractor={(item) => String(item)}
renderItem={({ item }) => (
<Text style={{ padding: 18 }}>Post #{item}</Text>
)}
/>
</Tabs.Tab>
<Tabs.Tab name="bookmarks" label="Bookmarks">
<Tabs.ScrollView>
<Text style={{ padding: 18 }}>Saved tutorials and resources.</Text>
</Tabs.ScrollView>
</Tabs.Tab>
</Tabs.Container>
</>
);
}FlashList Adapter For Large Lists
FlashList and LegendList use subpath exports. This keeps those packages optional.
import { Text, View } from 'react-native';
import { Tabs } from 'react-native-collapsible-tab';
import { TabFlashList } from 'react-native-collapsible-tab/flash-list';
const notifications = Array.from({ length: 1000 }, (_, index) => ({
id: `notification-${index}`,
title: `Notification ${index + 1}`,
}));
function NotificationHeader() {
return (
<View style={{ padding: 20, backgroundColor: '#ffffff' }}>
<Text style={{ fontSize: 24, fontWeight: '700' }}>Activity</Text>
<Text>Recent mentions, replies, and system updates.</Text>
</View>
);
}
export function ActivityTabs() {
return (
<Tabs.Container renderHeader={() => <NotificationHeader />}>
<Tabs.Tab name="all" label="All">
<TabFlashList
data={notifications}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Text style={{ padding: 16 }}>{item.title}</Text>
)}
/>
</Tabs.Tab>
<Tabs.Tab name="mentions" label="Mentions">
<Tabs.FlatList
data={notifications.slice(0, 20)}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Text style={{ padding: 16 }}>{item.title}</Text>
)}
/>
</Tabs.Tab>
</Tabs.Container>
);
}Pull To Refresh With Header Offset
The native refresh spinner needs different handling on iOS and Android. Android lists keep the offset clamped at zero, so use the native RefreshControl with a header offset. iOS bounce creates a negative scroll offset, so a custom spinner below the header gives better control.
import { Platform, RefreshControl, Text } from 'react-native';
import {
Tabs,
useCurrentTabScrollY,
useHeaderMeasurements,
} from 'react-native-collapsible-tab';
function RefreshableList() {
const scrollY = useCurrentTabScrollY();
const { height } = useHeaderMeasurements();
return (
<Tabs.FlatList
data={[1, 2, 3, 4]}
keyExtractor={(item) => String(item)}
refreshControl={
Platform.OS === 'android' ? (
<RefreshControl
refreshing={false}
onRefresh={() => {}}
progressViewOffset={height}
/>
) : undefined
}
renderItem={({ item }) => (
<Text style={{ padding: 16 }}>Refresh item {item}</Text>
)}
/>
);
}Use scrollY.value on iOS to position your custom spinner. Clamp negative values before mapping the offset to width, opacity, or progress.
Component Props And API Reference
Tabs.Container
| Prop | Type | Default | Use |
|---|---|---|---|
renderHeader | () => ReactNode | None | Renders the collapsible header. Header height is measured automatically. |
renderTabBar | (props: TabBarRenderProps) => ReactNode | DefaultTabBar | Replaces the default tab bar. |
minHeaderHeight | number | 0 | Keeps part of the header visible when collapsed. |
headerBackgroundColor | string | '#fff' | Sets the solid background behind the header and tab bar. |
headerContainerStyle | StyleProp<ViewStyle> | None | Adds styles to the animated header wrapper. |
containerStyle | StyleProp<ViewStyle> | None | Adds styles to the outer container. |
initialTabName | string | First tab | Sets the first focused tab. |
lazy | boolean | false | Mounts tab content on first focus. |
renderLazyPlaceholder | ({ name, index }) => ReactNode | null | Renders content for unmounted lazy tabs. |
revealHeaderOnScroll | boolean | false | Reveals the header as soon as the active list scrolls upward. |
snapThreshold | number | null | null | Snaps the header open or closed after release. |
onIndexChange | (index: number) => void | None | Runs after a tab switch settles. |
onTabChange | ({ prevIndex, index, prevTabName, tabName }) => void | None | Runs after a settled tab change with tab names. |
pagerProps | PagerView props | None | Passes props to the underlying pager. |
Container Ref Methods
Use useRef<CollapsingTabsRef> when your screen needs imperative control.
| Method | Use |
|---|---|
jumpToTab(name, animated?) | Moves to a tab by name. |
setIndex(index, animated?) | Moves to a tab by index. |
getFocusedTab() | Returns the focused tab name. |
getCurrentIndex() | Returns the active tab index. |
scrollToTop(animated?) | Scrolls the active tab to the top. |
scrollAllToTop() | Scrolls every registered tab to the top. |
Tabs.Tab
| Prop | Type | Use |
|---|---|---|
name | string | Stable tab identity for scroll memory and imperative tab jumps. |
label | string? | Display label for the tab bar. Defaults to name. |
lazy | boolean? | Overrides container-level lazy mounting for one tab. |
swipeEnabled | boolean? | Turns pager swiping off while this tab has focus. |
Keep name stable and simple. Use label for localized or display-only text.
Scrollable Components
| Component | Import Path | Notes |
|---|---|---|
Tabs.ScrollView | react-native-collapsible-tab | Use for static tab content. |
Tabs.FlatList | react-native-collapsible-tab | Use for standard lists. |
Tabs.SectionList | react-native-collapsible-tab | Use for grouped native lists. |
TabFlashList | react-native-collapsible-tab/flash-list | Use with FlashList v2 and New Architecture. |
TabLegendList | react-native-collapsible-tab/legend-list | Use with LegendList. |
The list adapters own onScroll. Keep custom scroll logic inside the library hooks or wrapper components instead of passing your own list-level onScroll.
Hooks
| Hook | Return | Use |
|---|---|---|
useHeaderScrollY() | SharedValue<number> | Reads the collapsed header distance in pixels. |
useCollapseProgress() | SharedValue<number> | Reads normalized collapse progress from 0 to 1. |
useHeaderMeasurements() | { top: SharedValue, height: number } | Reads header measurements for offsets and custom UI. |
useCurrentTabScrollY() | SharedValue<number> | Reads the raw scroll offset inside the current tab. |
useActiveTabScrollY() | SharedValue<number> | Reads the active tab scroll offset from header or tab bar code. |
useFocusedTab() | SharedValue<string> | Reads the focused tab name on the UI thread. |
useAnimatedTabIndex() | SharedValue<number> | Reads fractional pager position for tab indicators. |
useIsTabFocused(name) | boolean | Reads focus state in JavaScript. |
useTabIndex() | number | Reads the current tab index in JavaScript. |
Use these hooks inside Tabs.Container. Header, tab bar, and tab content components are valid locations.
Alternatives and Related Resources
- Collapsible Header And Custom Refresh Control For React Native
- React Native Animated Parallax Header
- 7 Best Parallax Scroll Components For React & React Native
FAQs
Q: Which dependencies are required?
A: Install react-native-collapsible-tab, then add react-native-reanimated, react-native-gesture-handler, and react-native-pager-view. FlashList and LegendList are optional.
Q: Does it work in Expo?
A: Yes. It supports Expo, including Expo Go. FlashList v2 still requires the New Architecture.
Q: Why does the refresh spinner appear behind the header?
A: The lists are padded below the header. On Android, set progressViewOffset to the header height. On iOS, use a custom spinner driven by the negative pull offset.
Q: Does it support React Native Web?
A: No. The library depends on react-native-pager-view, which targets native iOS and Android.
Q: Where should persistent header controls go?
A: Put persistent controls in renderTabBar. Header content collapses away, while the tab bar remains pinned.
Q: Can I use FlashList?
A: Yes, import TabFlashList from react-native-collapsible-tab/flash-list. FlashList v2 and New Architecture are required.
Q: My header flickers or shows blank space when I switch tabs. What’s wrong?
A: That behavior is typical of other collapsible tab views. This library decouples the header from tab offsets so the header never jumps. If you still see flicker, make sure you are using the latest version and have set headerBackgroundColor.





