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

@rnw-community/react-native-collapsible-header

v2.18.0

Published

Generic Reanimated collapsible header for React Native

Readme

React Native Collapsible Header

A generic, slot-based collapsible header powered by React Native Reanimated. The package animates header geometry and crossfades caller-owned expanded and collapsed content from a scroll offset it either receives as a prop or wires automatically through CollapsibleHeaderProvider. It works with any vertical scrollable — ScrollView, FlatList, SectionList, FlashList, or LegendList — because the only integration contract is a Reanimated SharedValue scroll offset.

npm version coverage npm downloads

Use cases

| Hero header | Navigation chrome | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | | | A rich summary (balance, profile, hero) crossfades into a compact row as content scrolls. Persistent actions stay tappable in both states. | A large screen title collapses into a small centered title between persistent leading/trailing icons — the iOS large-title pattern, so the header stops distracting from content. |

Both come from the monorepo's example app (Expo and bare React Native targets, src/component/header-demo-screen.tsx and src/component/settings-demo-screen.tsx) — copy those screens as integration templates. The example also carries the Maestro E2E suite that validates the header through the accessibility tree, including the react-native-screens freezeOnBlur return case.

Installation

npm install @rnw-community/react-native-collapsible-header react-native-reanimated

React Native Reanimated is a peer dependency and must be installed by the host application. Follow the official Reanimated setup guide.

  • Reanimated 4 applications also install react-native-worklets and configure react-native-worklets/plugin.
  • Reanimated 3 applications configure react-native-reanimated/plugin.

See the official Reanimated 3 migration guide and compatibility table when choosing a version. This package does not install native Reanimated or Worklets code on behalf of the host application.

React Compiler

The published output is precompiled with the React Compiler targeting React 18+, so every consumer gets automatically memoized header components — including applications that do not run the compiler themselves (the compiler never processes node_modules, so precompilation is the only way library code benefits). The react-compiler-runtime dependency ships with the package and supports React 18 and 19. The package's own test suite runs fully compiled with panicThreshold: 'all_errors', so any Rules-of-React violation fails the build instead of shipping. Reanimated supports the React Compiler from 3.17.2 — the peer floor — and this package uses the compiler-safe .get()/.set() shared-value API exclusively.

Quick start

CollapsibleHeaderProvider owns the scroll wiring: the header and the scrollable connect through context, so no manual scrollY plumbing is needed. useCollapsibleHeaderScroll hands the scrollable its onScroll handler and animated ref.

import React from 'react';
import { View } from 'react-native';
import Animated from 'react-native-reanimated';

import {
    CollapsibleHeader,
    CollapsibleHeaderProvider,
    useCollapsibleHeaderScroll,
} from '@rnw-community/react-native-collapsible-header';

const AccountScrollView = () => {
    const { onScroll, scrollRef } = useCollapsibleHeaderScroll();

    return <Animated.ScrollView ref={scrollRef} onScroll={onScroll} scrollEventThrottle={16} />;
};

const AccountScreen = () => (
    <CollapsibleHeaderProvider>
        <View style={{ flex: 1 }}>
            <CollapsibleHeader
                expandedHeight={156}
                collapsedHeight={40}
                snap
                expandedContent={<ExpandedAccountSummary />}
                collapsedContent={<CompactAccountSummary />}
                persistentContent={<HeaderActions />}
                backgroundStyle={{ backgroundColor: '#fff' }}
            />
            <AccountScrollView />
        </View>
    </CollapsibleHeaderProvider>
);

Safe-area padding, content layout, colors, typography, and scroll ownership remain the consumer's responsibility.

Manual scroll wiring

Pass scrollY directly to drive the header from a scroll offset you already own — no provider required. This is also the escape hatch for scrollables the provider cannot reach.

const AccountScreen = () => {
    const scrollY = useSharedValue(0);
    const onScroll = useAnimatedScrollHandler(event => {
        scrollY.set(event.contentOffset.y);
    });

    return (
        <View style={{ flex: 1 }}>
            <CollapsibleHeader
                scrollY={scrollY}
                expandedHeight={156}
                collapsedHeight={40}
                expandedContent={<ExpandedAccountSummary />}
                collapsedContent={<CompactAccountSummary />}
            />
            <Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16} />
        </View>
    );
};

CollapsibleHeader

CollapsibleHeader renders caller-owned expanded and collapsed content in animated layers, with optional persistent content mounted once above both transition layers. It clamps offsets before collapseStart to the expanded state and offsets after collapseStart + collapseDistance to the collapsed state. Only the visible transition layer receives pointer events and accessibility focus — the hidden layer is removed from the accessibility tree, so screen readers never announce both layers. Persistent content uses box-none.

Every layer is transparent to touches it does not own: the background layer uses none, the persistent layer and the visible transition layer use box-none, and the height-animated shell that hosts them all defaults to box-none too, so a tap on empty header space reaches the scrollable underneath instead of dying on the header rectangle. Descendants stay tappable — box-none excludes only the shell itself. Pass headerStyle={{ pointerEvents: 'auto' }} to make the shell swallow those taps; the caller value wins because headerStyle is merged after the default.

Content taller than the shrinking header can paint outside it mid-transition; pass headerStyle={{ overflow: 'hidden' }} to clip (clipping also cuts shadows, which is why it is not the default).

When a testID is set, every layer derives its own: {testID}-background, {testID}-header, {testID}-expanded, {testID}-collapsed, and {testID}-persistent (when persistent content exists). Query them in React Native Testing Library with { includeHiddenElements: true } — the invisible layer is accessibility-hidden by design — or target them directly from Maestro/Detox flows.

CollapsibleHeaderProps

| Prop | Type | Description | | --------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------- | | scrollY | SharedValue<number> | Caller-owned scroll offset; omit to use the nearest CollapsibleHeaderProvider. | | expandedContent | ReactNode | Content visible at the expanded endpoint. | | collapsedContent | ReactNode | Content visible at the collapsed endpoint. | | persistentContent | ReactNode | Content mounted once above both transition layers for actions or shared chrome. | | expandedHeight | number | Positive expanded header height. | | collapsedHeight | number | Positive collapsed header height, not greater than expandedHeight. | | collapseDistance | number | Scroll distance of the transition; defaults to expandedHeight - collapsedHeight. | | collapseStart | number | Non-negative scroll offset where collapse begins; defaults to 0. | | mode | 'flow' \| 'overlay' | flow participates in layout; overlay pins the header above the scrollable. | | snap | boolean | Snaps to the nearest endpoint when scrolling settles mid-transition; needs a provider. | | stretchOnOverscroll | boolean | Stretches the header height while the scrollable overscrolls above its top edge. | | motion | Partial<CollapsibleHeaderMotionConfig> | Optional normalized transition thresholds and endpoint transforms. | | headerStyle | StyleProp<ViewStyle> | Style for the height-animated header layer. | | backgroundStyle | StyleProp<ViewStyle> | Style for the background fade layer. | | expandedContentContainerStyle | StyleProp<ViewStyle> | Style for the expanded content layer. | | collapsedContentContainerStyle | StyleProp<ViewStyle> | Style for the collapsed content layer. | | persistentContentContainerStyle | StyleProp<ViewStyle> | Style for the persistent content layer mounted above both transition layers. |

The interface also accepts standard React Native ViewProps, except children; use the content slots instead.

CollapsibleHeaderProvider

Owns the scroll wiring — a scrollY shared value, a Reanimated scroll handler, and an animated scroll ref — and shares it through context. Descendant headers fall back to the provider's scrollY when the prop is omitted, and snap requires the provider because snapping drives the registered scrollable via scrollTo.

The provider also reads Reanimated's useReducedMotion() and snaps instantly instead of animating whenever the system "reduce motion" accessibility setting is on. Nothing is configurable here on purpose: an animated snap is an unrequested motion the user did not initiate, so it follows the platform setting.

syncNativeScrollOffset

scrollY is written from scroll events, so a navigator that restores a screen's native scroll offset without emitting one — pop back to an already-scrolled screen without freezeOnBlur — leaves scrollY stale and the header expanded over scrolled content until the next scroll. syncNativeScrollOffset (default false) additionally mirrors the true native offset into scrollY via Reanimated's useScrollOffset, which covers that restore.

It is opt-in because mirroring requires resolving the scroll ref to a real host instance: attaching the provider ref to a component that exposes only an imperative handle (getScrollableNode/scrollToOffset, a supported attachment) never resolves to one and throws inside Reanimated's observer. Enable it only when the provider ref always lands on a real scrollable (Animated.ScrollView, Animated.FlatList, or an exposed inner animated ref):

<CollapsibleHeaderProvider syncNativeScrollOffset>{/* screens */}</CollapsibleHeaderProvider>

Snap requires a mounted CollapsibleHeader

The provider owns the snap slot, but a CollapsibleHeader fills it: a header rendered with snap registers its geometry (collapseStart and collapseStart + collapseDistance) into the provider's registry on mount and clears it on unmount. A provider with no mounted snapping header therefore never snaps — scrolling settles wherever it lands. This is deliberate: snap endpoints are header geometry, so there is nothing to snap to without a header. Conditionally unmounting the header (a tab switch, a freezeOnBlur screen) turns snapping off for as long as it is gone, and remounting restores it.

One provider per scrollable

Mount a provider per screen, inside the screen — never once around a navigator. A provider holds exactly one scrollY, one scroll handler, one scroll ref, and one snap slot, so its unit of identity is a single scrollable:

  • Correct: each screen mounts its own provider. Sibling screens then have fully independent scroll state, so scrolling one never moves another's header — including when a screen is frozen by react-native-screens freezeOnBlur and returned to later.
  • Wrong: one provider wrapping several screens. They share one scrollY, so whichever screen scrolls last drives every header, and both screens' snapping headers compete for the same slot.

Several headers in one screen may share a provider — they animate from the same offset, which is exactly what you want for, say, a hero header plus a sticky sub-header. Only one of them may set snap: snapping is a property of the scrollable, not of the header, and a second snapping header registering different geometry throws rather than silently overriding the first. Passing a scrollY prop together with snap also throws, because the two scroll sources could disagree and snap the list without moving the header.

useCollapsibleHeaderScroll

Returns the provider-owned wiring for attaching a scrollable: { scrollY, onScroll, scrollRef }. Attach onScroll (and scrollRef when snapping) to any Reanimated-animated scrollable — Animated.ScrollView, Animated.FlatList, an animated SectionList, FlashList, or LegendList. Throws when no CollapsibleHeaderProvider ancestor exists. See the recipes for the per-list wiring.

CollapsibleHeaderScrollRef

The type of scrollRef. It is an AnimatedRef that additionally satisfies React's Ref<T> for every T, so the same ref attaches to lists whose ref prop is a component instance (Animated.FlatListRef<FlatList>), a createAnimatedComponent wrapper, or an imperative handle object (FlashList's FlashListRef, LegendList's LegendListRef) — with no cast at the call site. Name it when the scrollable lives in a child component and the ref travels as a prop:

interface TransactionListProps {
    readonly scrollRef: CollapsibleHeaderScrollRef;
    readonly onScroll: ScrollHandlerProcessed;
}

const TransactionList = ({ scrollRef, onScroll }: TransactionListProps) => (
    <AnimatedFlashList ref={scrollRef} onScroll={onScroll} data={transactions} renderItem={renderItem} />
);

Snapping resolves the underlying native scrollable from whatever instance the list hands the ref: Reanimated reads getScrollableNode() / getNativeScrollRef() when the instance exposes them, which is how handle-based lists stay snappable. When a list exposes its inner animated ScrollView ref separately (LegendList's refScrollView), prefer that prop — it is the most direct target for scrollTo.

useCollapsibleHeaderProgress

Returns the collapse progress as a SharedValue<number>0 fully expanded through 1 fully collapsed — available to any content rendered inside the header's slots. Drive custom slot animations from it without forking the package:

const Avatar = () => {
    const progress = useCollapsibleHeaderProgress();
    const avatarStyle = useAnimatedStyle(() => ({
        transform: [{ scale: interpolate(progress.get(), [0, 1], [1, 0.5]) }],
    }));

    return <Animated.Image source={avatar} style={avatarStyle} />;
};

getCollapsibleHeaderContentInsetStyle

Builds the content-container inset for overlay mode: getCollapsibleHeaderContentInsetStyle(156) returns { paddingTop: 156 }. Apply it to the scrollable's contentContainerStyle so content starts below the pinned header. With the default collapseStart of 0 and the default collapseDistance (expandedHeight - collapsedHeight), the content edge stays exactly aligned with the shrinking header. A positive collapseStart breaks that alignment — content scrolls up while the header still sits at its expanded height, so it slides under the header by collapseStart points. Pass getCollapsibleHeaderContentInsetStyle(expandedHeight + collapseStart) when delaying the collapse.

DefaultCollapsibleHeaderMotionConfig

The baseline motion preset. Spread it when building named presets so omitted fields stay original-compatible:

const subtleMotion = { ...DefaultCollapsibleHeaderMotionConfig, expandedScale: 1, expandedTranslateY: 0 };

Every field carries the value applied when motion is omitted, except pointerEventsSwitchProgress: an omitted switch progress resolves to the cross-fade midpoint of the thresholds actually in use, while spreading the preset passes its 0.5 explicitly.

CollapsibleHeaderMotionConfig

Motion progress fields are normalized within the active collapse interval [collapseStart, collapseStart + collapseDistance]. A progress value of 0 maps to the expanded endpoint, and 1 maps to the collapsed endpoint. Omitted motion fields use the defaults below, preserving the original animation behavior.

| Field | Default | Description | | -------------------------------- | ------- | ---------------------------------------------------------------------------- | | expandedOpacityEndProgress | 0.6 | Progress where expanded content finishes fading from opacity 1 to 0. | | collapsedOpacityStartProgress | 0.5 | Progress where collapsed content begins fading from opacity 0 to 1. | | backgroundOpacityStartProgress | 0.7 | Progress where the background begins fading from opacity 0 to 1. | | pointerEventsSwitchProgress | derived | Progress where pointer events and accessibility focus switch between layers. | | expandedTranslateY | -20 | Expanded content translateY at the collapsed endpoint. | | expandedScale | 0.9 | Expanded content scale at the collapsed endpoint; must be greater than 0. | | collapsedTranslateY | 10 | Collapsed content translateY at the fade-in start endpoint. |

pointerEventsSwitchProgress defaults to the cross-fade midpoint, (collapsedOpacityStartProgress + expandedOpacityEndProgress) / 2, so the layer that owns pointer events and accessibility focus is always the layer the user can actually see. A fixed switch point combined with a late collapsedOpacityStartProgress hands interaction and the accessibility tree to a layer that is still fully transparent, leaving no title exposed for that part of the scroll. When the two fades do not overlap — equal or inverted thresholds, so the derived midpoint lands exactly where the expanded layer reaches zero opacity — the expanded layer stops owning interaction at that point and the collapsed layer takes over, so the guarantee holds for every threshold pair rather than only for overlapping cross-fades. Setting the field explicitly wins over the derived midpoint; with the default thresholds the derived value is 0.55.

Opacity progress fields must be between 0 and 1, and collapsedOpacityStartProgress must be less than or equal to expandedOpacityEndProgress. Translation endpoints must be finite numbers.

CollapsibleHeaderMode

'flow' (default) keeps the header in normal layout flow — content below moves as the header height animates. 'overlay' pins the header absolutely at the top so the scrollable renders underneath; pair it with getCollapsibleHeaderContentInsetStyle and give the header a zIndex via style when siblings stack above it. Overlay mode keeps the per-frame height animation inside the absolutely positioned header subtree instead of re-laying out the whole screen.

Recipes

FlatList

Animated.FlatList ships with Reanimated — attach the wiring directly:

const TransactionList = () => {
    const { onScroll, scrollRef } = useCollapsibleHeaderScroll();

    return (
        <Animated.FlatList
            ref={scrollRef}
            data={transactions}
            renderItem={renderItem}
            onScroll={onScroll}
            scrollEventThrottle={16}
        />
    );
};

SectionList

Reanimated has no built-in animated SectionList; wrap it once at module scope so the component identity is stable:

const AnimatedSectionList = Animated.createAnimatedComponent(SectionList<Transaction>);

const TransactionSections = () => {
    const { onScroll, scrollRef } = useCollapsibleHeaderScroll();

    return (
        <AnimatedSectionList
            ref={scrollRef}
            sections={sections}
            renderItem={renderItem}
            onScroll={onScroll}
            scrollEventThrottle={16}
        />
    );
};

Replace any JS-thread onScroll on the list with this worklet handler — running the header off a JS-thread callback reintroduces the frame drops the package exists to avoid.

FlashList

Wrap FlashList the same way. Its ref is an imperative handle rather than a component instance, which the ref contract accepts; snapping still reaches the native scrollable through the handle's getScrollableNode().

const AnimatedFlashList = Animated.createAnimatedComponent(FlashList<Transaction>);

const TransactionList = () => {
    const { onScroll, scrollRef } = useCollapsibleHeaderScroll();

    return (
        <AnimatedFlashList
            ref={scrollRef}
            data={transactions}
            renderItem={renderItem}
            onScroll={onScroll}
            scrollEventThrottle={16}
        />
    );
};

LegendList

@legendapp/list publishes its own Reanimated build. Pass the ref through refScrollView — it targets the inner animated ScrollView, the exact view scrollTo drives — and keep the list's own ref free for imperative calls:

import { AnimatedLegendList } from '@legendapp/list/reanimated';

const TransactionList = () => {
    const { onScroll, scrollRef } = useCollapsibleHeaderScroll();

    return (
        <AnimatedLegendList
            refScrollView={scrollRef}
            data={transactions}
            renderItem={renderItem}
            onScroll={onScroll}
            scrollEventThrottle={16}
        />
    );
};

FlashList and LegendList are not dependencies of this package — the ref contract is structural, so nothing needs to be installed for the types above to line up.

Any other scrollable

For a list that neither exposes a scrollable-resolving ref nor an inner ScrollView ref, render the animated ScrollView yourself through renderScrollComponent (both FlashList and LegendList support it) and attach scrollRef there — or drop scrollRef entirely and keep only onScroll, which animates the header while leaving snap off.

react-native-web

The package is plain Reanimated + View layers and renders on react-native-web without platform-specific code — pointer events, accessibility hiding (aria-hidden), and transforms all map to their DOM equivalents through Reanimated's web support.

Testing consumers

Consumer Jest suites should use Reanimated's official setup:

require('react-native-reanimated').setUpTests();

See the Reanimated testing guide for matcher and timer configuration. Note that the hidden transition layer is removed from the accessibility tree, so React Native Testing Library queries for content inside it need { includeHiddenElements: true }.

License

This library is licensed under the MIT License.