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

expo-router-sticky-tabs

v0.1.6

Published

Instagram-quality sticky headers, collapsible layouts and swipeable top tabs for Expo Router. Headless, Reanimated-powered, New Architecture ready.

Readme

expo-router-sticky-tabs

Instagram-quality sticky headers, collapsible layouts and swipeable top tabs for Expo Router.

Headless. Reanimated-powered. New Architecture ready. It feels like a first-party Expo Router API — <Tabs>, <Tabs.Screen>, <Tabs.Header>, <Tabs.TabBar> — and never replaces the router. Expo Router stays the single source of truth for routes, URLs, deep links and history. This library only adds UI, layout, scroll/header synchronization and animation.

// app/(profile)/_layout.tsx
import { Tabs } from 'expo-router-sticky-tabs';

export default function ProfileLayout() {
  return (
    <Tabs>
      <Tabs.Header collapsible sticky>
        <ProfileHeader />
      </Tabs.Header>

      <Tabs.TabBar />

      <Tabs.Screen name="posts" href="/posts" options={{ title: 'Posts' }} />
      <Tabs.Screen name="reels" href="/reels" options={{ title: 'Reels', badge: 3 }} />
      <Tabs.Screen name="tagged" href="/tagged" options={{ title: 'Tagged' }} />
    </Tabs>
  );
}

The pager that lays out the tab screens is rendered automatically — there is no slot or container to add.


Why

Building an Instagram/Threads/TikTok-style profile on Expo Router means reconciling two things that fight each other:

  • A swipeable pager needs every tab mounted at once so you can swipe between them.
  • Expo Router's Slot renders one active route at a time.

expo-router-sticky-tabs resolves this by building on Expo Router's headless tab primitives (expo-router/ui). Expo Router owns the routing state; a pager renders the mounted route screens; swipes and taps are synced back to the router. You get real routes (deep links, browser history, router.navigate) and buttery swipe + collapse. See docs/DESIGN_DECISIONS.md.

Features

  • 🧭 Real Expo Router routes — each tab is a file. Deep links, history and params just work.
  • 👆 Native swipe via react-native-pager-view (gestures, velocity, RTL, a11y, virtualization).
  • 📌 Collapsible + sticky header driven entirely by Reanimated shared values on the UI thread.
  • 🔄 Per-tab scroll sync — each tab keeps its own scroll position, header state and indicator, restored on return (just like Instagram).
  • 📜 Drop-in list wrappersTabs.ScrollView, Tabs.FlatList, Tabs.SectionList, Tabs.FlashList.
  • 🎯 Animated indicator — pager-offset interpolation, width + colour interpolation, spring, custom renderers.
  • Zero unnecessary re-renders — separate zustand stores + shared values; the hot path never touches React state.
  • Accessible — tab/tablist roles, selected state, Reduce Motion support.
  • 🧩 Strict TypeScript, strong generics, no any, full IntelliSense.
  • 📱 iOS · Android · Expo Go · New Architecture · Fabric · Hermes · React 19.

Installation

npx expo install expo-router-sticky-tabs \
  expo-router react-native-reanimated react-native-gesture-handler \
  react-native-pager-view react-native-safe-area-context react-native-screens zustand

@shopify/flash-list is optional — install it only if you use <Tabs.FlashList>.

1. Babel (Reanimated)

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    // Reanimated 4: the worklets plugin must be listed last.
    plugins: ['react-native-worklets/plugin'],
  };
};

Using Reanimated 3? Use 'react-native-reanimated/plugin' instead.

2. Root layout providers

// app/_layout.tsx
import { Stack } from 'expo-router';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaProvider>
        <Stack screenOptions={{ headerShown: false }} />
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}

Quick start (Expo Router file layout)

app/
  _layout.tsx              # GestureHandlerRootView + SafeAreaProvider + Stack
  (profile)/
    _layout.tsx            # export default → <Tabs> … </Tabs>
    posts.tsx              # a tab screen (uses <Tabs.FlatList>)
    reels.tsx
    tagged.tsx

Each tab screen renders one of the synced list wrappers:

// app/(profile)/posts.tsx
import { Tabs } from 'expo-router-sticky-tabs';

export default function Posts() {
  return (
    <Tabs.FlatList
      data={data}
      numColumns={3}
      keyExtractor={(i) => String(i)}
      renderItem={({ item }) => <Cell item={item} />}
    />
  );
}

That's it — scroll one tab, switch away and back: the offset, header collapse and indicator are all restored.

API

<Tabs> / <Tabs.Provider> / TabsRoot

| Prop | Default | Description | | --- | --- | --- | | children | — | Layout children plus <Tabs.Screen> declarations. | | initialRouteName | First registered tab | The tab to focus initially. | | minHeaderHeight | 0 | Minimum collapsed header height in px. | | disableAnimation | false | Disables animated transitions and also applies under Reduce Motion. | | swipeEnabled | true | Allow swiping between tabs (the pager is rendered automatically). | | overdrag | false | Allow overscroll bounce at the pager edges. | | pagerStyle | — | Style applied to the internal pager container. |

<Tabs.Screen>

| Prop | Default | Description | | --- | --- | --- | | name | — | Route name; must match the Expo Router child route. | | href | — | Destination href used for deep links and tab presses. | | options | — | Tab-bar/header metadata such as title, badge, icon and headerShown. | | lazy | true | Defers mounting until the tab is first focused. | | disabled | false | Disables interaction and dims the tab. | | keepAlive | true | Keeps the screen mounted after blur instead of freezing it. | | headerShown | true | Shows the collapsible header for this screen. | | initialParams | — | Params merged into the route when first navigated to. | | listeners | — | Navigation listeners for route events. | | title | options.title or name | Shortcut for the tab label. | | badge | options.badge | Shortcut for the tab badge. | | icon | options.icon | Shortcut for the tab icon. |

<Tabs.Header>

| Prop | Default | Description | | --- | --- | --- | | collapsible | true | Allows the header to collapse while scrolling. | | sticky | true | Keeps the header pinned while the content scrolls. | | parallax | false | Applies subtle parallax/fade while collapsing. | | blur | false | Reserved for blur-based header treatments. | | safeArea | true | Adds the safe-area top inset. | | animated | true | Enables the animated collapse/expand behavior. | | dynamicHeight | true | Updates the animated height as the header measures. | | snap | false | Enables snap-style collapse behavior. | | floating | false | Renders the header as a floating overlay. | | children | — | Custom header content. | | style | — | Style applied to the header content container. | | background | — | Full-bleed background layer rendered behind the content. |

<Tabs.TabBar>

| Prop | Default | Description | | --- | --- | --- | | scrollable | false | Allows horizontal scrolling when there are many tabs. | | fixed | false | Forces even-width, non-scrolling tabs. | | style | — | Bar container style. | | contentContainerStyle | — | Inner row/scroll container style. | | tabStyle | — | Style applied to each tab button. | | labelStyle | — | Style applied to the tab label text. | | activeColor | '#000' | Active tab label/icon color. | | inactiveColor | '#8e8e8e' | Inactive tab label/icon color. | | indicatorStyle | — | Style applied to the built-in indicator. | | showIndicator | true | Shows the built-in indicator. | | renderTab | — | Fully custom tab renderer. | | renderIndicator | — | Fully custom indicator renderer. |

<Tabs.Indicator>

| Prop | Default | Description | | --- | --- | --- | | style | — | Style for the indicator view. | | interpolateWidth | true | Interpolates the width from tab measurements. | | colors | — | Optional color interpolation stops for the indicator. | | spring | false | Uses Reanimated spring behavior instead of tracking the pager 1:1. | | children | — | Custom renderer for the indicator. |

List wrappers

| Component | Props | Default / behavior | | --- | --- | --- | | <Tabs.ScrollView> | Same props as React Native ScrollView | Auto-synced to the header and scroll restoration. | | <Tabs.FlatList> | Same props as React Native FlatList | Auto-synced to the header and scroll restoration. | | <Tabs.SectionList> | Same props as React Native SectionList | Auto-synced to the header and scroll restoration. | | <Tabs.FlashList> | Same props as Shopify FlashList | Auto-synced to the header and scroll restoration. |

Rendered outside <Tabs> (e.g. a list component reused on a non-tab screen), each wrapper transparently falls back to its plain primitive — same props and ref, no scroll-sync injection — so it never throws.

<Tabs.Lazy>

| Prop | Default | Description | | --- | --- | --- | | children | — | Content to render once the tab has been focused. | | fallback | null | Content shown before first focus. | | unmountOnBlur | false | Unmounts children when the tab blurs instead of keeping them alive. |

<Tabs.Group>

| Prop | Default | Description | | --- | --- | --- | | children | — | Nested screens and layout children. | | screenOptions | undefined | Options merged into each nested <Tabs.Screen>. |

Hooks

useTabs()

| Return value | Default | Description | | --- | --- | --- | | tabs | — | All registered tabs in order. | | activeName | null | The focused tab name. | | activeIndex | — | The focused tab index. | | switchTab(name) | — | Switches to a tab by name. | | setPage(index) | — | Moves the pager imperatively. |

useCurrentTab()

| Return value | Default | Description | | --- | --- | --- | | tab | undefined | The currently focused tab metadata. | | name | null | The currently focused tab name. | | index | — | The currently focused tab index. |

usePager()

| Return value | Default | Description | | --- | --- | --- | | position | — | Continuous pager position as a shared value. | | activeIndex | — | Settled active index as a shared value. | | pageCount | — | Number of registered pages. | | isDragging | false | Whether the pager is currently being dragged. | | isSettling | false | Whether the pager is settling after a gesture. | | setPage(index) | — | Moves the pager to a page index. |

useHeader()

| Return value | Default | Description | | --- | --- | --- | | height | 0 | Measured full header height. | | tabBarHeight | 0 | Measured tab bar height. | | config | Header defaults | Resolved header behavior configuration. | | headerOffset | — | Shared collapse offset. | | headerHeight | — | Shared header height value. | | minHeaderHeight | — | Shared minimum collapsed header height. |

useIndicator()

| Return value | Default | Description | | --- | --- | --- | | pagerPosition | — | Continuous pager position for custom indicators. | | activeIndex | — | Settled active index. | | tabLayouts | — | Measured tab geometry as a shared value. |

useScrollSync()

| Return value | Default | Description | | --- | --- | --- | | scrollHandler | — | Reanimated scroll handler for the list. | | animatedRef | — | Animated ref for imperative scrolling. | | topInset | — | Top padding required above the list content. | | initialOffset | 0 | Initial scroll offset restored from the shared collapse state. | | scrollEventThrottle | 16 | Recommended scroll event throttle. |

useTabMeasurements()

| Return value | Default | Description | | --- | --- | --- | | SharedValue<TabLayout[]> | — | Measured tab geometry for indicator rendering. |

useCollapsibleHeader()

| Return value | Default | Description | | --- | --- | --- | | translateStyle | — | Animated transform style for the header/tab bar. | | parallaxStyle | — | Optional parallax/fade style. | | progress | — | Collapse progress from 0 to 1. | | config | Header defaults | Resolved header behavior configuration. |

useTabsContext() / useTabScreen()

| Return value | Default | Description | | --- | --- | --- | | Internal context values | — | Advanced composition hooks for custom integrations inside a <Tabs> tree. |

Documentation

Example app

A full Instagram-style profile lives in example/:

cd example
npx expo start

Compatibility

| | Version | |---|---| | Expo SDK | 54+ (built and tested on 57) | | React | 19 | | React Native | 0.79+ (New Architecture / Fabric / Hermes) | | expo-router | 3.5+ | | Platforms | iOS · Android · Expo Go |

Web is on the roadmap (native pagers don't render on web; a scroll-snap implementation is planned).

License

MIT © Raheem Adebayo