React Native Collapsible Header Tabs Component

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-tab

Install the required peer dependencies with Expo:

npx expo install react-native-reanimated react-native-gesture-handler react-native-pager-view

Add optional list backends only when your screen uses those adapters:

npx expo install @shopify/flash-list
npm install @legendapp/list

Use 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

PropTypeDefaultUse
renderHeader() => ReactNodeNoneRenders the collapsible header. Header height is measured automatically.
renderTabBar(props: TabBarRenderProps) => ReactNodeDefaultTabBarReplaces the default tab bar.
minHeaderHeightnumber0Keeps part of the header visible when collapsed.
headerBackgroundColorstring'#fff'Sets the solid background behind the header and tab bar.
headerContainerStyleStyleProp<ViewStyle>NoneAdds styles to the animated header wrapper.
containerStyleStyleProp<ViewStyle>NoneAdds styles to the outer container.
initialTabNamestringFirst tabSets the first focused tab.
lazybooleanfalseMounts tab content on first focus.
renderLazyPlaceholder({ name, index }) => ReactNodenullRenders content for unmounted lazy tabs.
revealHeaderOnScrollbooleanfalseReveals the header as soon as the active list scrolls upward.
snapThresholdnumber | nullnullSnaps the header open or closed after release.
onIndexChange(index: number) => voidNoneRuns after a tab switch settles.
onTabChange({ prevIndex, index, prevTabName, tabName }) => voidNoneRuns after a settled tab change with tab names.
pagerPropsPagerView propsNonePasses props to the underlying pager.

Container Ref Methods

Use useRef<CollapsingTabsRef> when your screen needs imperative control.

MethodUse
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

PropTypeUse
namestringStable tab identity for scroll memory and imperative tab jumps.
labelstring?Display label for the tab bar. Defaults to name.
lazyboolean?Overrides container-level lazy mounting for one tab.
swipeEnabledboolean?Turns pager swiping off while this tab has focus.

Keep name stable and simple. Use label for localized or display-only text.

Scrollable Components

ComponentImport PathNotes
Tabs.ScrollViewreact-native-collapsible-tabUse for static tab content.
Tabs.FlatListreact-native-collapsible-tabUse for standard lists.
Tabs.SectionListreact-native-collapsible-tabUse for grouped native lists.
TabFlashListreact-native-collapsible-tab/flash-listUse with FlashList v2 and New Architecture.
TabLegendListreact-native-collapsible-tab/legend-listUse 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

HookReturnUse
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)booleanReads focus state in JavaScript.
useTabIndex()numberReads 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

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.

Tags:

Add Comment