# react-native-collapsible-tab — full documentation for LLMs
> 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.
This file concatenates the full docs so an agent can install or migrate in one fetch.
Canonical site: https://rnct.scannertechs.com · npm: https://www.npmjs.com/package/react-native-collapsible-tab · GitHub: https://github.com/JassiSingh08/react-native-collapsible-tab
## Install
```bash
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
```
---
## Introduction
_A collapsible header tab view for React Native with per-tab scroll memory and a jump-free header._
See: https://rnct.scannertechs.com/docs/introduction
react-native-collapsible-tab gives you a collapsing header above a set of swipeable tabs. Each tab owns its scroll position, and the header never jumps when you switch tabs — because the header position is decoupled from tab scroll offsets.
Built on react-native-reanimated, react-native-gesture-handler and react-native-pager-view. All animation runs on the UI thread. Works with Reanimated 3 and 4, old and New Architecture (the FlashList adapter requires New Architecture), and Expo (including Expo Go).
```tsx
import { Tabs } from 'react-native-collapsible-tab';
function Profile() {
return (
} // measured automatically
minHeaderHeight={insets.top} // stays visible when collapsed
lazy // mount tabs on first focus
snapThreshold={0.5} // optional: snap open/closed
>
}
keyExtractor={(item) => item.id}
/>
);
}
```
The header is measured automatically — there is no headerHeight prop. Omit renderHeader entirely for a plain pinned tab bar.
---
## Installation
_Install the package and its peer dependencies._
See: https://rnct.scannertechs.com/docs/installation
```bash
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
```
Optional list backends — only if you use their adapters:
```bash
npx expo install @shopify/flash-list # v2+, New Architecture only
npm install @legendapp/list
```
Your app must be wrapped in (Expo Router does this for you).
> **Note:** Requirements: react-native-reanimated ≥ 3.6, react-native-gesture-handler ≥ 2, react-native-pager-view ≥ 6. iOS & Android only — no web (pager-view is native-only).
---
## Tabs
_Each tab has a stable name and an optional display label._
See: https://rnct.scannertechs.com/docs/tabs
Each child of is a . The name is the tab’s permanent identity — scroll memory and jumpToTab key off it — while label is what the user sees. Keeping name ASCII-simple avoids the sync bugs other libraries hit with localized tab text.
### Wrapping Tabs.Tab in your own component
The container detects children by their name prop, not by element type. So you can wrap Tabs.Tab in your own component — just forward name (and children) to the element you return.
```tsx
function MyTab({ name, label, children }) {
return (
{children}
);
}
```
#### Reference
- `name` (`string`): Stable identity, used for scroll memory and jumpToTab. Keep it ASCII-simple; localized text goes in label.
- `label` (`string?`): Display text for the tab bar. Defaults to name.
- `lazy` (`boolean?`): Per-tab override of the container's lazy.
- `swipeEnabled` (`boolean?`): While this tab is focused, disables pager swiping — for a horizontal carousel inside the tab.
---
## How the header collapses
_The rules that keep the header glued to content — and jump-free._
See: https://rnct.scannertechs.com/docs/collapse-rules
Scrolling down collapses the header in sync with the content. Scrolling up (the default) keeps the header collapsed until the content top reaches it again, then expands it in sync — so the header never detaches from content and never leaves a gap.
With revealHeaderOnScroll, any upward delta expands the header immediately (Twitter-style). When snap is enabled, it animates whichever transition keeps the content gapless.
> **⚠ Gotcha — Translucent headers need a solid background:** The header needs a solid headerBackgroundColor because per-tab scroll memory means deep-scrolled content can legitimately sit underneath an expanded header. That trade is exactly what makes tab switches jump-free.
> **⚠ Gotcha — Animate header content in place:** A custom header animation should fade or scale in place. Don’t translate header content upward as it collapses, or it rides past minHeaderHeight into the safe-area / status-bar region and overlaps it. Content that must stay visible while the header collapses (a progress bar, a search field) belongs in the tab bar via renderTabBar — the header scrolls away, the tab bar stays pinned.
---
## Snap
_Settle the header fully open or closed when released mid-collapse._
See: https://rnct.scannertechs.com/docs/snap-threshold
By default the header stays wherever the scroll leaves it. Set snapThreshold to make a partially-collapsed header animate to its nearest resting state on release — a polished, app-like feel.
```tsx
{tabs}
```
> **Note:** Snap is a plain shared-value animation — it never deadlocks the UI thread (a known failure mode of offset-driven snap on the New Architecture).
#### Reference
- `snapThreshold` (`number | null`): When set (0..1), a header released mid-collapse animates fully open or closed depending on how far it crossed the threshold. Default null (no snap).
---
## Reveal on scroll
_Bring the header back on any upward scroll, Twitter-style._
See: https://rnct.scannertechs.com/docs/reveal-on-scroll
Two reveal modes. The default keeps the header attached to content: it expands only when you scroll back to the top of the list. With revealHeaderOnScroll, the header pops back the instant you scroll up — like Twitter/X.
```tsx
{tabs}
```
#### Reference
- `revealHeaderOnScroll` (`boolean`): When true, any upward scroll delta expands the header immediately. Default false — the header re-appears only when content scrolls back up to it.
---
## Lazy mounting
_Mount tab content only on first focus, with a placeholder._
See: https://rnct.scannertechs.com/docs/lazy
With lazy, a tab’s content isn’t created until you first visit it — cheaper startup for screens with many or heavy tabs. Jumping to a far tab mounts only that destination, not every tab along the way (and onTabChange fires once, for the settled destination).
```tsx
}>
{tabs}
```
> **Note:** lazy defers the first mount, but a visited tab then stays mounted for the session. To cap memory across many tabs, combine it with windowConfig.
#### Reference
- `lazy` (`boolean`): Mount tab content on first focus. Swiping pre-mounts the neighbor; tapping a far tab mounts only the destination — never the tabs in between. Default false.
- `renderLazyPlaceholder` (`({ name, index }) => ReactNode`): Shown for unmounted lazy tabs. Default null.
---
## Windowed memory
_Cap how many tabs stay mounted — 60 tabs, only 3 live._
See: https://rnct.scannertechs.com/docs/window-config
lazy defers a tab’s first mount, but once visited it stays mounted — fine for a handful of tabs, costly for dozens of media-heavy lists. windowConfig caps it: only the focused tab plus `behind` left and `ahead` right stay live; the rest tear down their native views (the bulk of the memory — list cells, decoded images) while keeping React state and scroll position for instant restore.
```tsx
{tabs}
// 60 tabs, but only the focused one ±1 stay mounted
```
Use ahead / behind ≥ 1 so the swipe target is already live before the gesture finishes. The window is recomputed when a tab switch settles (not per frame), and it composes with lazy.
> **Note:** is stable in React 19.2+. On older React the prop is ignored with a one-time dev warning (every visited tab stays mounted, as before) — so it’s safe to set unconditionally.
#### Reference
- `windowConfig` (`{ ahead: number; behind: number }`): Keep only the focused tab plus `behind` tabs left and `ahead` right mounted; the rest hide via React’s , freeing native views while keeping React state and scroll position. Needs React 19.2+.
---
## Dynamic tabs & ref
_Add/remove tabs at runtime and drive the container imperatively._
See: https://rnct.scannertechs.com/docs/dynamic-and-ref
Tabs can be added or removed at runtime — scroll offsets are keyed by name, so existing tabs keep their position across the change. A ref exposes imperative controls for the container.
```tsx
const ref = useRef(null);
{tabs};
ref.current?.jumpToTab('media');
ref.current?.scrollAllToTop();
```
#### Reference
- `jumpToTab(name, animated?)` (`method`): Focus a tab by name.
- `setIndex(index, animated?)` (`method`): Focus a tab by index.
- `scrollToTop(animated?)` (`method`): Scroll the focused tab back to the top.
- `scrollAllToTop()` (`method`): Scroll every tab back to the top.
- `getFocusedTab() / getCurrentIndex()` (`method`): Read the current focus (name / index).
---
## List adapters
_Drop-in FlatList, ScrollView, SectionList, FlashList v2 & LegendList._
See: https://rnct.scannertechs.com/docs/lists
Each adapter is a drop-in replacement with the same props as the underlying component. It automatically pads content below the header + tab bar, guarantees short content can still fully collapse the header, restores saved offsets when a lazy tab mounts, sets sensible scrollIndicatorInsets / progressViewOffset, and feeds the header collapse + snap logic.
```tsx
import { TabFlashList } from 'react-native-collapsible-tab/flash-list';
import { TabLegendList } from 'react-native-collapsible-tab/legend-list';
```
### Sticky section headers
Sticky SectionList headers stick to the real viewport top, which sits under the collapsible header until it collapses. This matches native sticky behavior — design your section headers with that in mind.
> **Note:** Building your own adapter? The contract is small — see useTabContentStyle, useRegisterTabList, useRestoreTabOffset, useTabScrollLifecycle, and the LegendList.tsx source (~100 lines).
#### Reference
- `Tabs.ScrollView / FlatList / SectionList` (`component`): Same props as the underlying component, minus onScroll (the adapter owns it).
- `TabFlashList` (`from '.../flash-list'`): FlashList v2 (New Architecture only). maintainVisibleContentPosition is disabled by default — it issues corrective scrolls that fight tab-switch sync; pass your own to opt back in.
- `TabLegendList` (`from '.../legend-list'`): LegendList adapter.
---
## Pull to refresh
_A platform-split recipe — iOS and Android overscroll differently._
See: https://rnct.scannertechs.com/docs/pull-to-refresh
Because content is padded below the header, the native RefreshControl spinner renders at the content origin — tucked behind the header. The robust recipe differs by platform, because the two platforms overscroll differently.
> **⚠ Gotcha — Android — lists don’t bounce:** The offset stays clamped at 0 (you get a stretch/glow), so an offset-driven custom pull can’t work. Use the native RefreshControl and let the adapter’s default progressViewOffset (= header height) push its spinner below the header, or set it yourself.
> **⚠ Gotcha — iOS — lists bounce:** contentOffset.y goes negative past the top. Read that pull distance from useCurrentTabScrollY() and drive your own spinner pinned in the visible area (below the header) — the native iOS spinner would be hidden behind the header.
>
```tsx
const scrollY = useCurrentTabScrollY(); // negative on iOS = pull distance
const { height } = useHeaderMeasurements();
: undefined // iOS: render a custom spinner driven by scrollY instead
}
/>
```
---
## Custom tab bar
_Replace the default tab bar — or pin content that survives collapse._
See: https://rnct.scannertechs.com/docs/custom-tab-bar
Pass renderTabBar for a fully custom bar — pill buttons, a progress indicator, anything. The tab bar stays pinned while the header collapses, so it’s also the right home for content that must remain visible (a reading-progress bar, a search field).
### DefaultTabBar styling
When you don’t pass renderTabBar, DefaultTabBar is used. It’s accessible (tablist / tab roles, selected state) and stylable: scrollable (default true; false = equal-width tabs), backgroundColor, activeColor, inactiveColor, indicatorColor, style, tabStyle, labelStyle, indicatorStyle, and renderLabel.
#### Reference
- `renderTabBar` (`(props: TabBarRenderProps) => ReactNode`): Render your own tab bar. Default: DefaultTabBar.
---
## Hooks
_Read scroll, collapse progress and focus on the UI thread._
See: https://rnct.scannertechs.com/docs/hooks
All hooks must be used inside (header, tab bar, and tab content all qualify). The shared-value hooks let you drive header animations — parallax, fade, progress bars — on the UI thread.
> **⚠ Gotcha — Scroll offsets go negative on overscroll:** useCurrentTabScrollY() / useActiveTabScrollY() return the list’s raw contentOffset.y, which goes negative on iOS when the user bounces past the top (Android stays clamped at 0). If you map a scroll offset to a width, opacity, or progress, clamp the low end too — not just the high end. Setting bounces={false} hides the symptom, but clamping is the real fix and keeps the native bounce.
>
```tsx
// ❌ negative offset → negative width; during bounce-back the bar can flash full
width: `${Math.min((scrollY.value / TOTAL) * 100, 100)}%`
// ✅ clamp both ends so overscroll reads as 0%
width: `${Math.max(0, Math.min((scrollY.value / TOTAL) * 100, 100))}%`
```
#### Reference
- `useHeaderScrollY()` (`SharedValue`): Pixels the header has collapsed. For header animations (parallax, fade).
- `useCollapseProgress()` (`SharedValue 0..1`): Normalized collapse progress. For normalized header animations.
- `useHeaderMeasurements()` (`{ top: SharedValue, height: number }`): Header position + height. Migration-compatible with collapsible-tab-view.
- `useCurrentTabScrollY()` (`SharedValue`): Raw offset of the tab you’re inside.
- `useActiveTabScrollY()` (`SharedValue`): Raw offset of the focused tab, usable in the header.
- `useFocusedTab()` (`SharedValue`): Focused tab name on the UI thread.
- `useAnimatedTabIndex()` (`SharedValue`): Fractional pager position. For tab bar indicators.
- `useIsTabFocused(name) / useTabIndex()` (`boolean / number`): JS-state focus — re-renders the component on tab switch.
---
## API reference
_Every prop and ref method, in one place._
See: https://rnct.scannertechs.com/docs/api-reference
The full surface. Each prop also has its own focused page under the sections above — this is the flat reference.
### Imperative ref
useRef exposes: jumpToTab(name, animated?), setIndex(index, animated?), getFocusedTab(), getCurrentIndex(), scrollToTop(animated?), scrollAllToTop().
> **Note:** Web is not supported (react-native-pager-view is native-only).
#### Reference
- `renderHeader` (`() => ReactNode`): Collapsible header. Omit for a plain pinned tab bar. Height is measured automatically.
- `renderTabBar` (`(props) => ReactNode · default DefaultTabBar`): Custom tab bar.
- `minHeaderHeight` (`number · default 0`): Header px that stays visible when fully collapsed (e.g. safe-area top).
- `headerBackgroundColor` (`string · default '#fff'`): Solid backing behind header + tab bar.
- `initialTabName` (`string · default first tab`): Tab to focus on mount.
- `lazy` (`boolean · default false`): Mount tab content on first focus. Tapping a far tab mounts only the destination.
- `revealHeaderOnScroll` (`boolean · default false`): Any upward scroll reveals the header immediately (Twitter-style).
- `snapThreshold` (`number | null · default null`): When set (0..1), a header released mid-collapse animates fully open or closed.
- `windowConfig` (`{ ahead, behind }`): Cap mounted tabs to the focused tab ± window; the rest hide via . Needs React 19.2+.
- `onIndexChange` (`(index) => void`): Fires when a tab switch settles.
- `onTabChange` (`({ prevIndex, index, ... }) => void`): Same timing, richer payload. Never fires for intermediate pages.
- `headerContainerStyle` (`StyleProp`): Extra styles on the animated header wrapper.
- `containerStyle` (`StyleProp`): Styles for the outer container.
- `pagerProps` (`PagerView props`): Escape hatch to the underlying pager (keyboardDismissMode, overdrag, …).
---
## `` props (full reference)
| Prop | Type | Description |
| --- | --- | --- |
| `renderHeader` | `() => ReactNode` | Collapsible header. Omit for a plain pinned tab bar. Height is measured automatically. |
| `renderTabBar` | `(props) => ReactNode` | Custom tab bar. |
| `minHeaderHeight` | `number` | Header px that stays visible when fully collapsed (e.g. safe-area top). |
| `headerBackgroundColor` | `string` | Solid backing behind header + tab bar. |
| `initialTabName` | `string` | Tab to focus on mount. |
| `lazy` | `boolean` | Mount tab content on first focus. Tapping a far tab mounts only the destination. |
| `revealHeaderOnScroll` | `boolean` | Any upward scroll reveals the header immediately (Twitter-style). |
| `snapThreshold` | `number | null` | When set (0..1), a header released mid-collapse animates fully open or closed. |
| `windowConfig` | `{ ahead, behind }` | Cap mounted tabs to the focused tab ± window; the rest hide via . Needs React 19.2+. |
| `onIndexChange` | `(index) => void` | Fires when a tab switch settles. |
| `onTabChange` | `({ prevIndex, index, ... }) => void` | Same timing, richer payload. Never fires for intermediate pages. |
## Hooks (full reference)
| Hook | Returns | Use |
| --- | --- | --- |
| `useHeaderScrollY()` | `SharedValue` | Header animations (parallax, fade) |
| `useCollapseProgress()` | `SharedValue 0..1` | Normalized header animations |
| `useHeaderMeasurements()` | `{ top, height }` | Migration-compatible with collapsible-tab-view |
| `useCurrentTabScrollY()` | `SharedValue` | Raw offset of the tab you’re inside |
| `useActiveTabScrollY()` | `SharedValue` | Raw offset of the focused tab, usable in the header |
| `useFocusedTab()` | `SharedValue` | Focused tab name on the UI thread |
| `useAnimatedTabIndex()` | `SharedValue` | Fractional pager position (tab bar indicators) |
| `useIsTabFocused(name)` | `boolean` | JS-state focus (re-renders on switch) |