react-native-synced-tab-view
v1.0.1
Published
A synchronized tab view with a collapsible header for React Native.
Maintainers
Readme
react-native-synced-tab-view
Synchronized top tabs with a measured, collapsible header for React Native. Build profile, feed, and dashboard layouts where horizontal paging and vertical scroll positions stay aligned across tabs.
⚡ Powered by Reanimated + Worklets
This library uses react-native-reanimated and react-native-worklets to keep tab switching, header collapse, and scroll synchronization smooth across modern React Native apps.
- Smooth shared-value driven interactions: Active tab state and indicator movement are animated with Reanimated.
- Synchronized scrolling: Header and tab content offsets stay coordinated when users swipe between tabs.
- Modern compatibility: Built for current React Native projects using Reanimated
>= 4.
✨ Features
- Collapsible Header Layout: Render any header above your tabs and let the package measure and sync it automatically.
- Synced Tab Content: Keep vertical scroll state aligned while switching between tab pages.
- Ready-to-use Scroll Wrappers: Use
TabFlatList,TabSectionList, andTabScrollViewout of the box. - Custom List Support: Wrap third-party scrollers like FlashList or Legend List with
createSyncedTabScrollComponent. - Customizable Top Tabs: Style the indicator, labels, colors, and even provide a fully custom
renderTabItem. - Profile-style UI Friendly: Designed for layouts like social profiles, dashboards, and content hubs.
🧩 Compatibility
| Target | Supported Version |
| :---- | :---- |
| React Native | >= 0.76 |
| React | >= 18 |
| Reanimated | >= 4 |
| React Native Worklets | >= 0.7.0 |
| Android | Supported |
| iOS | Supported |
| Web | Not supported yet |
📦 Installation
Install the package and its peer dependencies:
yarn add react-native-synced-tab-view react-native-reanimated react-native-workletsPeer Dependency Setup
Make sure your project is already configured for react-native-reanimated. After installing dependencies, rebuild the native app. If you are using a bare iOS project, run:
cd ios && pod installIf you are using Expo, install versions compatible with your SDK and rebuild the app if needed.
🚀 Quick Start
import * as React from "react";
import {
StyleSheet,
Text,
View,
type ListRenderItemInfo,
} from "react-native";
import {
SyncedTabsLayout,
TabFlatList,
type TabItem,
type TabPageProps,
type TabViewTopTabsProps,
} from "react-native-synced-tab-view";
type DemoItem = {
id: string;
title: string;
};
const POSTS_DATA: DemoItem[] = Array.from({ length: 12 }, (_, index) => ({
id: `post-${index + 1}`,
title: `Post ${index + 1}`,
}));
const SAVED_DATA: DemoItem[] = Array.from({ length: 12 }, (_, index) => ({
id: `saved-${index + 1}`,
title: `Saved ${index + 1}`,
}));
const keyExtractor = (item: DemoItem) => item.id;
const DemoCard = React.memo(({ item }: { item: DemoItem }) => (
<View style={styles.card}>
<Text style={styles.cardTitle}>{item.title}</Text>
</View>
));
type FeedTabProps = Pick<TabPageProps, "index" | "width"> & {
data: DemoItem[];
};
const FeedTab = React.memo(function FeedTab({
index,
width,
data,
}: FeedTabProps) {
const renderItem = React.useCallback(
({ item }: ListRenderItemInfo<DemoItem>) => <DemoCard item={item} />,
[],
);
return (
<TabFlatList
index={index}
width={width}
data={data}
keyExtractor={keyExtractor}
contentContainerStyle={styles.listContent}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
/>
);
});
export default function ExampleScreen() {
const tabs = React.useMemo(
() =>
[
{
key: "posts",
name: "Posts",
component: ({ index, width }: TabPageProps) => (
<FeedTab index={index} width={width} data={POSTS_DATA} />
),
},
{
key: "saved",
name: "Saved",
component: ({ index, width }: TabPageProps) => (
<FeedTab index={index} width={width} data={SAVED_DATA} />
),
},
] satisfies TabItem[],
[],
);
return (
<View style={styles.container}>
<SyncedTabsLayout
header={
<View style={styles.header}>
<Text style={styles.title}>Profile</Text>
<Text style={styles.subtitle}>
Collapsible header content goes here.
</Text>
</View>
}
tabs={tabs}
initialTabIndex={0}
topTabsProps={TOP_TABS_PROPS}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
header: {
padding: 24,
backgroundColor: "#f3f4f6",
},
title: {
fontSize: 24,
fontWeight: "700",
color: "#111827",
marginBottom: 8,
},
subtitle: {
fontSize: 14,
color: "#4b5563",
},
listContent: {
padding: 16,
},
card: {
padding: 16,
borderRadius: 12,
backgroundColor: "#e5e7eb",
marginBottom: 12,
},
cardTitle: {
fontSize: 16,
fontWeight: "600",
color: "#111827",
},
tabText: {
fontSize: 14,
fontWeight: "600",
},
});
const TOP_TABS_PROPS = {
indicatorColor: "#111827",
indicatorHeight: 3,
tabTextStyle: styles.tabText,
} satisfies TabViewTopTabsProps;⚙️ API Reference
Exports
| Export | Description |
| :---- | :---- |
| SyncedTabsLayout | High-level layout component that wires the provider, measured header, top tabs, and pager together. |
| TabFlatList | Synchronized FlatList wrapper for tab content. |
| TabSectionList | Synchronized SectionList wrapper for tab content. |
| TabScrollView | Synchronized ScrollView wrapper for tab content. |
| createSyncedTabScrollComponent | Factory for wrapping third-party scrollable components. |
SyncedTabsLayout Props
| Prop | Type | Description | Default |
| :---- | :---- | :---- | :---- |
| header | React.ReactNode | Content rendered above the tabs and measured by the shared scroller. | Required |
| tabs | TabItem[] | Tab definitions shared between the tab bar and pager. | Required |
| width | number | Width of the tab bar and page area. | Screen width |
| initialTabIndex | number | Tab shown on first render. | 0 |
| topTabsProps | TabViewTopTabsProps | Props forwarded to the built-in top tabs component. | {} |
| scrollerProps | TabViewScrollerProps | Props forwarded to the horizontal pager scroller. | {} |
| tabsProviderProps | object | Shared scroll behavior options, including resetPreviousScrollingY. | { resetPreviousScrollingY: true } |
| scrollerContainerProps | object | Props for the outer synchronized vertical scroller. | — |
| headerContainerProps | object | Props for the measured header container. | — |
TabItem Shape
| Field | Type | Description |
| :---- | :---- | :---- |
| key | string \| number | Stable identity used for React keys and tab state when provided. |
| id | string \| number | Alternate stable identity if key is not used. |
| name | string | Label rendered in the top tabs. |
| component | (props: TabPageProps) => React.ReactNode | Function that renders the tab page. |
TabPageProps includes:
item: the currentTabItemindex: the tab indexwidth: the page widthisActive: a Reanimated shared value indicating whether the tab is active
Synced Scroll Components
| Component | Based On | Extra Props |
| :---- | :---- | :---- |
| TabFlatList<T> | FlatListProps<T> | index: number, width?: number |
| TabSectionList<ItemT, SectionT> | SectionListProps<ItemT, SectionT> | index: number, width?: number |
| TabScrollView | ScrollViewProps | index: number, width?: number |
TabFlatList and TabSectionList intentionally do not expose windowSize.
Common topTabsProps Options
| Prop | Type | Description | Default |
| :---- | :---- | :---- | :---- |
| tabItemStyle | StyleProp<ViewStyle> | Style applied to each tab item wrapper. | — |
| tabTextStyle | StyleProp<TextStyle> | Style applied to tab labels. | — |
| indicatorColor | string | Color of the active indicator. | "white" |
| indicatorHeight | number | Height of the indicator. | 4 |
| indicatorWidth | number | Width of the indicator. | 50 |
| showIndicator | boolean | Whether the indicator is visible. | true |
| renderTabItem | (props: TopTabItemRenderProps) => React.ReactElement | Custom tab item renderer. | — |
| renderIndicator | () => React.ReactElement | Custom indicator renderer. | — |
🧪 Custom Scroll Components
If you want to sync a third-party list, wrap it with createSyncedTabScrollComponent:
import { FlashList } from "@shopify/flash-list";
import { createSyncedTabScrollComponent } from "react-native-synced-tab-view";
const SyncedFlashList = createSyncedTabScrollComponent(FlashList);The wrapped component still forwards refs to the underlying list instance, so imperative methods remain available.
🛠 Why not wire this manually?
Profile-style tab layouts usually require several moving parts at the same time: a measured header, a horizontal pager, synchronized vertical offsets, and custom tab visuals. Wiring all of that by hand often leads to duplicated onScroll logic, ref coordination, and edge cases when switching tabs.
react-native-synced-tab-view packages those behaviors behind a small API so each tab can focus on rendering content instead of managing scroll synchronization.
📱 Example App
The repository includes an Expo example app with these demo flows:
- Base synchronized tabs example
- Example with saved tab position
- Instagram-style profile layout
- TikTok-style profile layout
- FlashList integration example
- Legend List integration example
You can keep this section as-is or replace it with screenshots, GIFs, or hosted demo links later.
⚠️ Known Issues
- Tab scroll-position reset and restore behavior should currently be considered experimental. On Android, it can flicker and may not be stable across every list adapter.
- When using a
LegendListadapter inside a tab together with position reset on tab switch (resetPreviousScrollingY: true), resetting the list can temporarily show a white background.
📌 Limitations
- This package currently supports Android and iOS. Web support is not available yet.
- The top section does not currently support a horizontal
FlatList. - If you need horizontal list content there, use a horizontal
FlashListor horizontalLegendListinstead.
💡 Recommendations
- For the best performance with a vertical list and
tabsProviderProps={{ resetPreviousScrollingY: false }}, preferLegendList. - If
resetPreviousScrollingYis enabled (true, the default), preferFlashList.
🧪 Local Development
yarn install
yarn start
yarn example:ios
yarn example:android
yarn typecheck
yarn build🧯 Troubleshooting
- Animations are not working: Verify
react-native-reanimatedis installed and configured correctly in your app, then rebuild the native project. - Scroll sync feels inconsistent after tab changes: Provide a stable
keyoridon everyTabItem, especially if tab order can change. - You need a different list implementation: Wrap it with
createSyncedTabScrollComponentinstead of syncing it manually. - You want the outer vertical scroll indicator visible: Pass
scrollerContainerProps={{ showsVerticalScrollIndicator: true }}.
📄 License
MIT © Mohammed B.
☕ Support
If you find this project useful and would like to support its ongoing development, consider buying me a coffee! Your support helps keep the creativity brewing and allows me to continue improving and maintaining this project. Thank you! ☕💖
