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.
tab-sync.mp4
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.
- 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.
| 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 |
Install the package and its peer dependencies:
yarn add react-native-synced-tab-view react-native-reanimated react-native-workletsMake 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.
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;| 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. |
| 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. | β |
| 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
| 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.
| 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. | β |
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.
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.
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.
- 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.
- 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.
- For the best performance with a vertical list and
tabsProviderProps={{ resetPreviousScrollingY: false }}, preferLegendList. - If
resetPreviousScrollingYis enabled (true, the default), preferFlashList.
yarn install
yarn start
yarn example:ios
yarn example:android
yarn typecheck
yarn build- 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 }}.
MIT Β© Mohammed B.
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! βπ
