npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-infinite-material-tab

v0.2.2

Published

Infinite scroll Material Design tab view for React Native — built on PagerView + Reanimated for native-grade performance. Zero JS thread work during swipe gestures.

Readme

react-native-infinite-material-tab

CI npm version npm downloads License: MIT TypeScript React Native Expo

Renovate enabled Dependabot CodeRabbit Pull Request Reviews Maestro E2E PRs Welcome GitHub stars

Infinite scroll tab view for React Native — built on PagerView + Reanimated for native-grade performance.

New Architecture ready | Expo 55+ compatible | Drop-in replacement for react-native-collapsible-tab-view

Architecture

┌─────────────────────────────────────────────────────┐
│  Tabs.Container                                     │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │  Header (optional, collapsible)               │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │  TabBar — ScrollView (smooth swipe)           │  │
│  │  ┌─────┬─────┬─────┬─────┬─────┐             │  │
│  │  │ Tab │ Tab │[Act]│ Tab │ Tab │  ← ∞ loop   │  │
│  │  └─────┴─────┴─────┴─────┴─────┘             │  │
│  │  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  ← Reanimated indicator    │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │  PagerView (native gestures)                  │  │
│  │  ┌─────────┬─────────┬─────────┐             │  │
│  │  │  Page   │ [Visible│  Page   │             │  │
│  │  │ (lazy)  │  Page]  │ (lazy)  │             │  │
│  │  └─────────┴─────────┴─────────┘             │  │
│  │  offscreenPageLimit=1 → only 3 pages mounted  │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
└─────────────────────────────────────────────────────┘

Lazy mount (lazy={true}) — pagerIndex-based (v0.2.0+)

With infiniteScroll=true the library generates tabs.length × BUFFER_MULTIPLIER virtual pages so the user can swipe forever without hitting the edge. That means multiple virtual pages share the same realIndex — pagerIndex 0, 5, 10, 15, … all map to the same tab.

lazy={true} tracks mount state by pagerIndex, not realIndex:

realIndex:   [0][1][2][3][4][0][1][2][3][4][0][1][2][3][4] ...
              ↑                       ↑
          pagerIndex 0          pagerIndex 5 (same realIndex 0)
              │                       │
      User reaches here       User swipes here
              ↓                       ↓
          renders content       renders content independently
              │                       │
      pagerIndex 5, 10, 15…   pagerIndex 0, 10, 15…
      stay empty until visited  stay empty until visited

Only virtual pages the user actually reaches render their children. Non-visited clones stay as empty <View> forever. This guarantees at most one HeavyContent mount per real tab, even under heavy list rendering, complex hook composition, or slow async data fetching inside the children.

v0.1.x had a critical bug here: mount state was tracked by realIndex, so a single tab activation triggered up to BUFFER_MULTIPLIER (=10) parallel HeavyContent mounts, saturating the JS thread with 400–750ms dispatch latency. v0.2.0 fixes this; no API changes required.

Why This Library?

Rendering Efficiency — Only What You See

Traditional ScrollView approach (❌ wasteful):
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │10 │11 │12 │13 │14 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
  ▲   ▲   ▲   ▲   ▲   ▲   ▲   ▲   ▲   ▲   ▲   ▲   ▲   ▲   ▲
  ALL 15 pages mounted in DOM simultaneously
  Memory: O(N × VIRTUAL_MULTIPLIER)  →  45 pages for 5 tabs!


This library with PagerView (✅ efficient):
                    ┌───┬───┬───┐
                    │ 3 │[4]│ 5 │
                    └───┴───┴───┘
                      ▲   ▲   ▲
                      prev cur next
  Only 3 pages mounted at any time (offscreenPageLimit=1)
  Memory: O(3)  →  constant regardless of tab count!

Infinite Loop — Clone & Jump Strategy

Page Layout (5 tabs):
┌──────────────────┬──────────────────┬──────────────────┐
│   Head Clones    │   Real Pages     │   Tail Clones    │
│  [0] [1] [2] [3] [4]│[0] [1] [2] [3] [4]│[0] [1] [2] [3] [4]│
└──────────────────┴──────────────────┴──────────────────┘
                    ↑ initialPage

Swipe left past clone[0]:             Swipe right past clone[4]:
  ┌──→ idle detected                    ┌──→ idle detected
  │    pendingJump = real[0]            │    pendingJump = real[4]
  │    setPageWithoutAnimation()        │    setPageWithoutAnimation()
  └──→ seamless! user sees no jump     └──→ seamless! user sees no jump

  No setTimeout ✓  No flicker ✓  Native-speed ✓

Thread Architecture — Async Follow Design

┌─────────────────────────┐    ┌─────────────────────────┐
│      UI Thread          │    │      JS Thread          │
│  (native, 60fps)        │    │  (React, after idle)    │
│                         │    │                         │
│  PagerView gestures     │    │  onPageSelected         │
│  Page transitions       │    │    → setActiveIndex     │
│  Reanimated indicator ◄─┼────┼──── withTiming          │
│  ScrollView tab swipe   │    │  Tab centering (scrollTo)│
│                         │    │  onTabChange (deferred) │
└─────────────────────────┘    └─────────────────────────┘

  Swipe gesture    → Native thread (PagerView, 60fps, zero JS)
  Tab bar scroll   → Native thread (ScrollView, 60fps)
  Indicator move   → UI thread (withTiming, after swipe completes)
  Tab centering    → JS thread (scrollTo, after swipe completes)
  onTabChange      → JS thread (deferred to idle)

  Key: swipe and tab don't wait for each other.
  The initiator runs at 60fps, the follower catches up afterward.

Tab Bar — Smooth Swipe with Virtual Loop

Tab Bar (ScrollView, ×3 virtual multiplier):
┌─────────────────────────────────────────────────────────────────┐
│  Set 1 (clone)     │  Set 2 (center)    │  Set 3 (clone)       │
│ [A][B][C][D][E]    │ [A][B][C][D][E]    │ [A][B][C][D][E]      │
└─────────────────────────────────────────────────────────────────┘
                      ↑ initial scroll position

  User swipes tab bar freely ← →
  Edge detected? → requestAnimationFrame → reset to center
  No setTimeout ✓  No jank ✓  Smooth momentum ✓

Tab indicator animation:
  ┌─────┬─────┬─────┬─────┬─────┐
  │  A  │  B  │ [C] │  D  │  E  │   activeIndex: 2
  └─────┴─────┴─────┴─────┴─────┘
              ▓▓▓▓▓                  ← Animated.View
                                       useSharedValue(x, width)
  Tab press C → D:                      withTiming(200ms)
  ┌─────┬─────┬─────┬─────┬─────┐
  │  A  │  B  │  C  │ [D] │  E  │
  └─────┴─────┴─────┴─────┴─────┘
                    ▓▓▓▓▓            ← slides smoothly

Dynamic Tab Width

Fixed width (❌ old):
┌──────────┬──────────┬──────────┬──────────┬──────────┐
│  Tech    │ Business │   AI     │  Sports  │  Music   │
│  100px   │  100px   │  100px   │  100px   │  100px   │
└──────────┴──────────┴──────────┴──────────┴──────────┘
  Wastes space on short labels, truncates long ones

Dynamic width (✅ new):
┌──────┬──────────┬─────┬────────┬───────┐
│ Tech │ Business │ AI  │ Sports │ Music │
│ 56px │   88px   │40px │  72px  │ 64px  │
└──────┴──────────┴─────┴────────┴───────┘
  Each tab measured via onLayout → pixel-perfect centering

Performance Comparison

                        This Library          ScrollView-based
                        ────────────          ────────────────
Page engine             PagerView (native)    ScrollView (JS)
Gesture tracking        UI thread             JS thread
Mounted pages           3 (constant)          N × multiplier
Tab indicator           Reanimated worklet    Conditional render
Edge reset              rAF + idle event      setTimeout(100ms)
Jump mechanism          setPageWithoutAnim    scrollTo + setTimeout
Tab item re-render      React.memo            Full re-render
Tab width               Dynamic (onLayout)    Fixed (100px)

                        ┌──────────────────────────────┐
Frame budget (16ms):    │                              │
                        │  ████░░░░░░░░░░░░  8ms  ✅  │  This library
                        │  ████████████████  16ms  ⚠️  │  ScrollView-based
                        │  ████████████████████ 22ms ❌│  (frame drop)
                        └──────────────────────────────┘

Features

  • PagerView — native page gestures, 60fps guaranteed
  • Infinite horizontal scroll for tabs and content
  • Reanimated indicator — smooth sliding animation on UI thread
  • Dynamic tab width — auto-measured via onLayout
  • Lazy renderinglazy={true} + offscreenPageLimit={1}; only the virtual pages the user actually reaches render their children (see Lazy mount section below)
  • Zero setTimeout — all timing via requestAnimationFrame + idle detection
  • Active tab center alignment — auto-scrolls with shortest-path algorithm
  • Collapsible header support
  • New Architecture (Fabric) ready
  • Expo 55+ compatible
  • Drop-in replacement for react-native-collapsible-tab-view
  • FlashList compatible
  • TypeScript first

Installation

npm install react-native-infinite-material-tab
# or
yarn add react-native-infinite-material-tab
# or
pnpm add react-native-infinite-material-tab

Peer Dependencies

npm install react-native-reanimated react-native-pager-view

| Package | Required | Purpose | |---------|----------|---------| | react-native-reanimated | Yes | Tab indicator animation (UI thread) | | react-native-pager-view | Yes | Native page gestures & transitions | | @shopify/flash-list | Optional | High-performance list in tab content |

Follow the setup guides:

Usage

Basic Example

import { Tabs } from 'react-native-infinite-material-tab';

function App() {
  return (
    <Tabs.Container
      infiniteScroll={true}
      tabBarCenterActive={true}
      onTabChange={(event) => console.log(event.tabName)}
    >
      <Tabs.Tab name="tech" label="Tech">
        <Tabs.FlatList
          data={newsItems}
          renderItem={({ item }) => <NewsCard item={item} />}
        />
      </Tabs.Tab>
      <Tabs.Tab name="business" label="Business">
        <Tabs.FlatList
          data={businessItems}
          renderItem={({ item }) => <NewsCard item={item} />}
        />
      </Tabs.Tab>
      {/* ... more tabs */}
    </Tabs.Container>
  );
}

With Collapsible Header

const HEADER_HEIGHT = 200;

function App() {
  return (
    <Tabs.Container
      renderHeader={() => (
        <View style={{ height: HEADER_HEIGHT }}>
          <Image source={require('./banner.png')} />
        </View>
      )}
      headerHeight={HEADER_HEIGHT}
    >
      <Tabs.Tab name="home" label="Home">
        <Tabs.ScrollView>
          <YourContent />
        </Tabs.ScrollView>
      </Tabs.Tab>
    </Tabs.Container>
  );
}

With FlashList

<Tabs.Tab name="feed" label="Feed">
  <Tabs.FlashList
    data={items}
    renderItem={({ item }) => <FeedCard item={item} />}
    estimatedItemSize={120}
  />
</Tabs.Tab>

Custom Tab Bar

import { Tabs, MaterialTabBar } from 'react-native-infinite-material-tab';

// Use built-in MaterialTabBar with customization
<Tabs.Container
  renderTabBar={(props) => (
    <MaterialTabBar
      {...props}
      activeColor="#F3BE21"
      inactiveColor="#86888A"
      indicatorStyle={{ height: 2 }}
    />
  )}
>
  {/* tabs */}
</Tabs.Container>

// Or build your own
function CustomTabBar({ tabs, activeIndex, onTabPress }: TabBarProps) {
  return (
    <View style={{ flexDirection: 'row' }}>
      {tabs.map((tab, index) => (
        <TouchableOpacity
          key={tab.name}
          onPress={() => onTabPress(index)}
        >
          <Text style={{ color: activeIndex === index ? 'blue' : 'gray' }}>
            {tab.label}
          </Text>
        </TouchableOpacity>
      ))}
    </View>
  );
}

API Reference

Tabs.Container

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | ReactNode | - | Tabs.Tab components | | renderHeader | () => ReactElement | - | Header above tabs | | renderTabBar | (props: TabBarProps) => ReactElement | - | Custom tab bar | | headerHeight | number | 0 | Header height (px) | | infiniteScroll | boolean | true | Enable infinite loop | | tabBarCenterActive | boolean | true | Auto-center active tab | | onTabChange | (event: TabChangeEvent) => void | - | Tab change callback | | onFocusedTabPress | (index: number) => void | - | Called when the already-active tab is pressed again (e.g. scroll to top) | | initialTabName | string | - | Initial active tab name | | pagerProps | Partial<PagerViewProps> | - | Props forwarded to PagerView | | containerStyle | StyleProp<ViewStyle> | - | Container style | | headerContainerStyle | StyleProp<ViewStyle> | - | Header wrapper style | | tabBarContainerStyle | StyleProp<ViewStyle> | - | Tab bar wrapper style | | offscreenPageLimit | number | 1 | PagerView offscreen pages (1=3 pages, 2=5 pages) | | lazy | boolean | false | Only mount tab content when nearby (reduces JS thread load for heavy tabs) | | debug | boolean | false | Enable debug logging (nearby/active/unmounted transitions) | | onDebugLog | (event: DebugLogEvent) => void | - | Debug log callback for app-side logging |

Tabs.Tab

| Prop | Type | Description | |------|------|-------------| | name | string | Unique tab identifier | | label | string | Tab label text | | children | ReactNode | Tab content |

MaterialTabBar

| Prop | Type | Default | Description | |------|------|---------|-------------| | activeColor | string | "#000" | Active tab text & indicator color | | inactiveColor | string | "#666" | Inactive tab text color | | scrollEnabled | boolean | true | Enable horizontal scroll | | indicatorStyle | StyleProp<ViewStyle> | - | Indicator style override | | labelStyle | StyleProp<TextStyle> | - | Label style override | | tabStyle | StyleProp<ViewStyle> | - | Tab item style override |

TabChangeEvent

interface TabChangeEvent {
  tabName: string;     // Active tab name
  index: number;       // Active tab index
  prevTabName: string; // Previous tab name
  prevIndex: number;   // Previous tab index
}

Hooks

| Hook | Returns | Description | |------|---------|-------------| | useCurrentTabScrollY() | SharedValue<number> | Current tab's scroll Y position | | useActiveTabIndex() | number | Currently active tab index | | useTabs() | Tab[] | Array of tab info | | useIsNearby(tabName) | boolean | Whether the tab is active or adjacent (for prefetching) | | useNearbyIndexes() | number[] | Array of active + adjacent tab indexes | | useTabsContext() | TabsContextValue | Full context value |

Migration from react-native-collapsible-tab-view

- import { Tabs } from 'react-native-collapsible-tab-view';
+ import { Tabs } from 'react-native-infinite-material-tab';

Add peer dependency:

npm install react-native-pager-view  # if not already installed

Requirements

  • Expo SDK 55+ (New Architecture only)
  • React Native >= 0.83
  • React >= 19.2
  • react-native-reanimated >= 3.0
  • react-native-pager-view >= 6.0

Contributing

Contributions are welcome! Please read our Contributing Guide for details.

License

MIT License - see the LICENSE file for details.

Author

johntips