Collapsible tabs that don't jump.
The smooth collapsible tab view for React Native — per-tab scroll memory, a jump-free header, and first-class FlashList & LegendList support.
- Reanimated 3 & 4
- Old & New Architecture
- Expo (incl. Expo Go)
- iOS & Android
Why it's different
Built to stay smooth
Every feature follows from one decision: decouple the header from tab scroll offsets. No force-scrolling, no sync hacks.
Jump-free header
The header position is its own animated value, driven only by the active tab’s scroll deltas. Switching tabs never moves it — no flicker, no blank space, no ghost header.
Per-tab scroll memory
Each tab keeps its own scroll offset, keyed by name. Switch away and back and you land exactly where you left — even across dynamic add/remove.
Every list, first-class
Drop-in adapters for FlatList, ScrollView, SectionList, FlashList v2 and LegendList — each pads, restores offsets, and feeds the collapse logic automatically.
Scrollable header, working buttons
A pan gesture on the header drives the active list (with release momentum) but only after 10px of movement — so taps still reach your buttons. The bug nobody else fixed.
Bounded tab memory
windowConfig caps how many tabs stay mounted — the rest free their native views via React’s <Activity> while keeping state and scroll position for instant restore. 60 tabs, only 3 live.
Runs on the UI thread
All animation runs on the UI thread via Reanimated — no shared-value reads during render, no Jest loops, no breakage on every Reanimated or Expo bump.
Works with every list — drop-in, same props:
Migrating?
The bugs you've been fighting, gone
The category’s classic bugs come from tying the header to tab scroll offsets and force-scrolling every tab to stay in sync. Decoupling them removes a whole class of issues by design.
Buttons in the header block scrolling (most-requested, never fixed)
Pan gesture + tap pass-through — both work
Blank space / ghost header on tab switch
Structurally impossible: header decoupled from offsets
Non-ASCII tab names break sync
`name` is pure identity; display text lives in `label`
Breaks on every Reanimated / Expo bump
Public APIs only, no render-time shared reads — v3 & v4
Jumping to a far tab mounts every tab in between
Only the destination mounts; intermediates stay placeholders
`onTabChange` fires for every intermediate tab
Fires once, for the settled destination
FlashList: header won’t collapse, short lists break
Dedicated v2 adapter with a real Animated.ScrollView
Dynamic tabs reset scroll positions
Offsets keyed by name survive add / remove
Quick start
Two components, that's the API
Wrap your tabs in a Container, give each a name, and use the drop-in scrollables. The header is measured for you.
1. Install
npm install react-native-collapsible-tab # peer deps (you likely have them): npx expo install react-native-reanimated \ react-native-gesture-handler react-native-pager-view
3. Cap memory on big tab sets
<Tabs.Container windowConfig={{ ahead: 1, behind: 1 }}>
{tabs}
</Tabs.Container>
// 60 tabs, but only the focused one ±1 stay mounted2. Use it
import { Tabs } from 'react-native-collapsible-tab';
function Profile() {
return (
<Tabs.Container
renderHeader={() => <ProfileHeader />} // measured automatically
minHeaderHeight={insets.top} // stays visible when collapsed
lazy // mount tabs on first focus
snapThreshold={0.5} // optional: snap open/closed
>
<Tabs.Tab name="posts" label="Posts">
<Tabs.FlatList
data={posts}
renderItem={({ item }) => <Post post={item} />}
keyExtractor={(item) => item.id}
/>
</Tabs.Tab>
<Tabs.Tab name="media" label="Media">
<Tabs.ScrollView>
<MediaGrid />
</Tabs.ScrollView>
</Tabs.Tab>
</Tabs.Container>
);
}