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-screen-chrome

v2.19.0

Published

Composable scroll-driven screen chrome for React Native and React Native Web

Readme

React Native Screen Chrome

Composable, safe-area-aware screen chrome for React Native and React Native Web. It paints collapsible titles, persistent navigation controls, progressive edge fades, and content insets around one scrollable, while all motion, scroll wiring, and collapse snapping are delegated to @rnw-community/react-native-collapsible-header and all visible content and product behavior stay consumer-owned.

npm version coverage

Installation

npm install @rnw-community/react-native-screen-chrome \
    @rnw-community/react-native-collapsible-header \
    expo-blur \
    react-native-reanimated react-native-safe-area-context

The native libraries and @rnw-community/react-native-collapsible-header are peer dependencies and must be installed by the host application: the application must resolve exactly one copy of the collapsible header, otherwise its scroll context has two identities and the chrome components fail to find their provider. Reanimated 4 applications also install react-native-worklets and configure react-native-worklets/plugin.

expo-blur must be >=55 <58: the blurMethod prop and the BlurMethod type this package passes and re-exports in EdgeFadePropsInterface only exist from that release, which renamed experimentalBlurMethod to blurMethod.

Complete example

import React from 'react';
import { Pressable, Text } from 'react-native';

import {
    CollapsibleHeader,
    CollapsibleHeaderBackdrop,
    CollapsibleHeaderSlot,
    CollapsibleHeaderTitleSlot,
    EdgeFade,
    ScreenChromeFrame,
    ScreenChromeProvider,
    ScreenChromeScrollView,
} from '@rnw-community/react-native-screen-chrome';

const SCREEN_CHROME_CONFIG = { snapToCollapse: true };

export const AccountsScreen = () => (
    <ScreenChromeProvider config={SCREEN_CHROME_CONFIG}>
        <ScreenChromeFrame>
            <ScreenChromeScrollView contentInsetTop={96} contentInsetBottom={48}>
                <Text>Consumer-owned content</Text>
            </ScreenChromeScrollView>
            <CollapsibleHeaderBackdrop />
            <EdgeFade position="bottom" />
            <CollapsibleHeader>
                <CollapsibleHeaderSlot>
                    <Pressable accessibilityRole="button">
                        <Text>Back</Text>
                    </Pressable>
                </CollapsibleHeaderSlot>
                <CollapsibleHeaderTitleSlot>
                    <Text>Accounts</Text>
                    <Text>Accounts</Text>
                </CollapsibleHeaderTitleSlot>
                <CollapsibleHeaderSlot>
                    <Pressable accessibilityRole="button">
                        <Text>Menu</Text>
                    </Pressable>
                </CollapsibleHeaderSlot>
            </CollapsibleHeader>
        </ScreenChromeFrame>
    </ScreenChromeProvider>
);

Structure and paint order

ScreenChromeProvider owns the merged and validated configuration plus the color scheme, and mounts a CollapsibleHeaderProvider that owns the scroll offset, scroll handler, animated scroll ref, and snap registry. ScreenChromeScrollView connects that wiring automatically; custom animated scrollables read onScroll, scrollRef, and scrollY from useCollapsibleHeaderScroll (exported by @rnw-community/react-native-collapsible-header), while useScreenChrome returns only { colorScheme, config }.

Mount one ScreenChromeProvider per scrollable, inside each screen, and never once around a navigator: a provider owns exactly one scroll offset and one snap slot, so a shared provider makes the last-scrolled screen drive every header. Several chrome components within one screen share one provider by design.

Render content first, decorative EdgeFade and CollapsibleHeaderBackdrop layers second, and interactive CollapsibleHeader chrome last. Native blur then samples the content beneath it while controls remain above decorative layers. ScreenChromeScrollView adds safe-area padding and caller-provided content insets; explicit consumer padding in contentContainerStyle remains last and wins.

CollapsibleHeader

Composes the persistent leading/trailing controls and the expanded/collapsed title layers of its three compound children into the generic @rnw-community/react-native-collapsible-header primitive, reading geometry and thresholds from useScreenChrome.

<CollapsibleHeader>
    <CollapsibleHeaderSlot>{/* leading control */}</CollapsibleHeaderSlot>
    <CollapsibleHeaderTitleSlot>
        {/* expanded title */}
        {/* collapsed title */}
    </CollapsibleHeaderTitleSlot>
    <CollapsibleHeaderSlot>{/* trailing control */}</CollapsibleHeaderSlot>
</CollapsibleHeader>

The header container is sized to safeAreaInsets.top + config.headerHeight, and both title layers and the persistent control row start at safeAreaInsets.top, so config.headerHeight is always the usable content height regardless of notch or dynamic-island depth. The primitive positions those layers absolutely inside the animated header, so the offset is a layer top rather than container padding — padding leaves absolute layers resolving against the full container box and centers their content over the inset instead of below it. It is the same composition ScreenChromeScrollView applies to contentInsetTop, so passing contentInsetTop={config.headerHeight} clears the overlay header exactly.

Pass motion to override individual per-layer animation windows or transforms derived from config — any key you set wins, the rest stay config-driven. This covers app-specific motion (for example an earlier background fade start, or expanded-layer scale) without widening the config surface:

<CollapsibleHeader motion={{ backgroundOpacityStartProgress: 0.7, expandedScale: 0.9 }}>{/* slots */}</CollapsibleHeader>

Both title layers default to centered content behind a 72pt gutter that keeps the title clear of the leading and trailing controls. expandedContentContainerStyle, collapsedContentContainerStyle and persistentContentContainerStyle are forwarded to the primitive and merged after that default, so a consumer style wins without needing to know or cancel the gutter value — an iOS-style left-aligned large title is just:

<CollapsibleHeader expandedContentContainerStyle={{ alignItems: 'flex-start', paddingHorizontal: 16 }}>
    {/* slots */}
</CollapsibleHeader>

Compound header contract

CollapsibleHeaderSlot, CollapsibleHeaderTitleSlot, and a second CollapsibleHeaderSlot must be direct children of CollapsibleHeader in that order. The title slot must directly contain the expanded title followed by the collapsed title. Fragments and extra wrapper components are rejected so slot discovery stays deterministic.

The shape above is a compound-component contract that TypeScript's type system cannot express: JSX children are ReactNode, so nothing in the type checker distinguishes three correctly ordered slots from an arbitrary fragment tree at compile time. CollapsibleHeader therefore validates its children at render time and throws a TypeError naming the violated rule (missing slot, wrong slot type, wrong title-layer count) instead of silently mounting a broken layout — a mispositioned or dropped slot without this check degrades into leading controls rendering as the title, or a header that mounts with no crash and no visible content. Four spec cases pin the accepted shape and every rejected one.

CollapsibleHeaderSlot

Renders one persistent leading or trailing control slot in a collapsible header, mounted once above both title transition layers.

<CollapsibleHeaderSlot>
    <Pressable accessibilityRole="button">
        <Text>Back</Text>
    </Pressable>
</CollapsibleHeaderSlot>

CollapsibleHeaderTitleSlot

Groups the direct expanded and collapsed title layers of a compound collapsible header; title opacity and pointer-event handoff between them are delegated to @rnw-community/react-native-collapsible-header.

<CollapsibleHeaderTitleSlot>
    <Text>Accounts</Text>
    <Text>Accounts</Text>
</CollapsibleHeaderTitleSlot>

CollapsibleHeaderBackdrop

Renders a top EdgeFade sized to headerBackdropHeight and aligned with the configured title-collapse thresholds, so native blur samples the content scrolling beneath the header.

<CollapsibleHeaderBackdrop />

Edge fades

Native edge fades use Masked View, Expo Linear Gradient, and Expo Blur. They ignore pointer events and accessibility traversal. Web edge fades use CSS mask images and backdrop filtering; scroll animation changes opacity while blur stays static. Native defaults use 150-point top and bottom bands, while web defaults use 76-pixel bands.

EdgeFade

Renders a decorative top or bottom blur band, scroll-animatable through scrollAnimation.

<EdgeFade position="bottom" height={96} scrollAnimation={{ opacityInputRange: [0, 80] }} />

On web only scrollAnimation.opacityInputRange animates: the band's backdrop-filter is a static blur derived from intensity, so intensityInputRange and maxIntensity have no effect there. Animated web blur is tracked in #591.

Android blur is opt-in through blurTarget. Since expo-blur@55 the dimezisBlurView methods only blur the background of a BlurTargetView, so blurMethod defaults to 'dimezisBlurView' when a blurTarget ref is supplied and to 'none' otherwise — expo-blur itself falls back to 'none' with a console warning when a targeted method has no target, and that fallback is what the default avoids:

const blurTarget = useRef<View>(null);

<BlurTargetView ref={blurTarget}>{content}</BlurTargetView>
<EdgeFade position="top" blurTarget={blurTarget} />;

EdgeFadePropsInterface

| Prop | Type | Description | | ----------------- | ---------------------------------- | ----------------------------------------------------------------------------------- | | position | EdgeFadePosition | Screen edge the band renders at. | | height | number | Band height; defaults to the provider config's fade height. | | intensity | number | Static blur intensity; ignored while scrollAnimation drives it. | | scrollAnimation | EdgeFadeScrollAnimationInterface | Optional scroll-driven opacity and blur intensity ranges. | | blurMethod | BlurMethod | Android Expo Blur rendering method; defaults from blurTarget. | | blurTarget | RefObject<View \| null> | Android BlurTargetView ref the band blurs; required by dimezisBlurView methods. |

EdgeFadeScrollAnimationInterface

Configures scroll-driven edge-fade opacity and blur intensity.

const scrollAnimation: EdgeFadeScrollAnimationInterface = {
    opacityInputRange: [0, 80],
    intensityInputRange: [0, 80],
    maxIntensity: 60,
};

EdgeFadePosition

Identifies the screen edge where a fade band is rendered: 'top' | 'bottom'.

ScreenChromeFrame

Provides the relative full-screen layout root for content, fades, and header chrome.

<ScreenChromeFrame>{children}</ScreenChromeFrame>

ScreenChromeHeader

Renders a static, non-collapsible header row with the same safe-area and paint-order contract as the collapsible one: absolute at the top, zIndex: 3, box-none, sized to insets.top + topInset + headerHeight so getScreenChromeHeaderMetrics predicts its footprint exactly. topInset adds app-owned padding beyond the safe area for screens that nest under an already-padded container. Requires a ScreenChromeProvider ancestor.

<ScreenChromeHeader testID="header" topInset={10} style={styles.headerBackground}>
    <Button onPress={goBack} title="Back" />
    <Text>{title}</Text>
</ScreenChromeHeader>

ScreenChromeProvider

Provides validated configuration and color scheme to screen chrome components around one scrollable.

<ScreenChromeProvider colorScheme="dark" config={{ snapToCollapse: true }}>
    {children}
</ScreenChromeProvider>

syncNativeScrollOffset is forwarded to the underlying CollapsibleHeaderProvider: it mirrors the native scroll offset into the shared scroll value so navigator offset restores without a scroll event keep the header in sync. It is opt-in because it requires the chrome scroll ref to resolve to a real host scrollable — see the collapsible-header readme for the full constraint.

Pass a stable config reference — a module-scope constant, or one owned by state — rather than the inline literal above. React Compiler memoizes the merge, validation, and context value on the identity of that prop, so a fresh literal on every parent render re-merges, re-validates, and re-broadcasts the context to every chrome consumer:

const SCREEN_CHROME_CONFIG = { snapToCollapse: true };

export const AccountsScreen = () => <ScreenChromeProvider config={SCREEN_CHROME_CONFIG}>{children}</ScreenChromeProvider>;

ScreenChromeScrollView

Connects an animated scroll view to the collapsible-header scroll wiring and safe-area content padding.

<ScreenChromeScrollView contentInsetTop={96} contentInsetBottom={48}>
    <Text>Consumer-owned content</Text>
</ScreenChromeScrollView>

Scroll is package-owned: onScroll and scrollEventThrottle come from the provider (useCollapsibleHeaderScroll and config.scrollEventThrottle) and are omitted from the props, so passing them is a type error instead of a silently discarded value. A consumer ref is accepted and fans out through mergeRefs alongside the chrome scroll ref, so imperative APIs like scrollTo and keyboard-aware wrappers keep working without detaching the header wiring.

contentInsetMode selects how the computed safe-area+chrome padding meets a consumer contentContainerStyle: 'replace' (the default, unchanged) lets consumer padding win, while 'additive' stacks the computed insets on top of consumer paddingTop/paddingBottom via addScrollContentInset and never injects horizontal padding:

<ScreenChromeScrollView contentInsetMode="additive" contentInsetTop={96} contentContainerStyle={{ paddingTop: 8 }} />

Change the event rate through <ScreenChromeProvider config={{ scrollEventThrottle: 8 }}>. The provider's onScroll is one processed Reanimated handler that also carries drag and snap events, so it is attached whole rather than wrapped — read the scroll position from the shared scrollY instead:

const { scrollY } = useCollapsibleHeaderScroll();

useAnimatedReaction(
    () => scrollY.get(),
    offsetY => {
        runOnJS(onOffsetChange)(offsetY);
    }
);

Every other ScrollView prop passes through untouched, including the scroll callbacks the package does not own (onMomentumScrollEnd, onContentSizeChange, …) and contentContainerStyle — consumer padding is applied after the safe-area padding and wins.

ScreenChromeContext

The React context that carries the resolved screen chrome value to package components and hooks; consumers read it through useScreenChrome rather than useContext(ScreenChromeContext) directly.

useScreenChrome

Reads the nearest ScreenChromeContext value and throws when no ScreenChromeProvider is mounted.

const { colorScheme, config } = useScreenChrome();

useScrollFadeStyle

Creates a clamped opacity style from the collapsible-header provider scroll value and therefore requires a ScreenChromeProvider ancestor.

const style = useScrollFadeStyle([0, 80], [1, 0]);

getScreenChromeHeaderMetrics

Computes the rendered header footprint from a header height, the device top inset, and an optional extra header top inset, matching the height the CollapsibleHeader and ScreenChromeHeader containers render. Pure and React-independent, so it works in non-component code and tests. Use the returned number as the scroll content's top inset so content starts below the header.

const headerTotalHeight = getScreenChromeHeaderMetrics(config.headerHeight, insets.top, topInset);

useScreenChromeHeaderMetrics

Returns getScreenChromeHeaderMetrics for the live provider config and device insets (no extra top inset — the collapsible container owns none), so screens offset their content from the rendered header without re-merging SCREEN_CHROME_DEFAULT_CONFIG. Requires a ScreenChromeProvider ancestor.

const headerTotalHeight = useScreenChromeHeaderMetrics();

ScreenChromeColorScheme

Selects the light or dark chrome palette for edge fades and backdrop blur tinting: 'light' | 'dark'. Being a plain union rather than a string enum, React Native's useColorScheme() result assigns to it directly — no cast, no mapping layer between the platform API and the provider prop.

const colorScheme: ScreenChromeColorScheme = useColorScheme() ?? 'light';

<ScreenChromeProvider colorScheme={colorScheme}>{children}</ScreenChromeProvider>;

ScreenChromeColorSetInterface

Defines the solid and translucent wash colors used by a chrome color scheme.

const lightColors: ScreenChromeColorSetInterface = { solid: 'rgba(255,255,255,0.42)', wash: 'rgba(255,255,255,0.08)' };

ScreenChromeConfigInterface

Configures screen chrome geometry, colors, fade masks, scroll throttling, and collapse thresholds. See Configuration and snapping for the threshold ordering rule and ScreenChromeDefaultConfig for the full default shape.

ScreenChromeConfigOverridesInterface

Overrides selected screen chrome defaults while preserving nested color schemes and mask-stop records; the shape ScreenChromeProvider's config prop accepts.

<ScreenChromeProvider config={{ headerHeight: 72, colors: { dark: { solid: 'black' } } }}>{children}</ScreenChromeProvider>

ScreenChromeContextValueInterface

The { colorScheme, config } shape returned by useScreenChrome.

ScreenChromeMaskStopInterface

Defines one color stop in an edge-fade mask.

const stop: ScreenChromeMaskStopInterface = { color: 'rgba(0,0,0,0.99)' };

Configuration and snapping

Provider overrides deep-merge light/dark colors and top/bottom mask stops. Geometry, blur intensity, throttle, mask positions, colors, and transition ordering are validated. The required threshold order is collapseStart <= smallTitleStart <= largeTitleEnd <= collapseEnd with a non-zero collapse interval.

Thresholds are mapped to normalized collapse progress and handed to the generic header as its motion config, so smallTitleStart and largeTitleEnd keep their meaning while the transition itself is owned upstream. The pointer-event and accessibility switch is deliberately left unset so the primitive derives it from the mapped cross-fade midpoint, which keeps the interactive, accessibility-exposed title the one that is actually visible for every threshold pair.

snapToCollapse is forwarded to the generic header's snap prop, which snaps the scrollable toward the nearest endpoint once a released scroll holds still for three frames — momentum events are never relied on, because a worklet-only scroll handler never receives them on Android. Snapping therefore requires a mounted CollapsibleHeader: the header registers the snap geometry with the provider, so a screen that enables snapToCollapse without rendering CollapsibleHeader scrolls freely. Snap animation currently ignores the reduced-motion setting.

ScreenChromeDefaultConfig

The platform default ScreenChromeConfigInterface: native (ScreenChromeDefaultConfig.ts) uses 150-point top and bottom fade bands and a 220-point header backdrop; web (ScreenChromeDefaultConfig.web.ts) uses 76-pixel bands and a 108-pixel backdrop. mergeScreenChromeConfig starts from this value.

ScreenChromeSharedDefaultConfig

The platform-independent subset of the default configuration — geometry, colors, mask stops, and throttling shared by the native and web ScreenChromeDefaultConfig variants.

assertValidScreenChromeConfig

Throws a property-specific error when a screen chrome config cannot drive stable scroll animations — non-finite or negative geometry, an invalid threshold order, a missing color scheme, or an out-of-range mask stop.

assertValidScreenChromeConfig(mergeScreenChromeConfig({ headerHeight: -1 })); // throws: headerHeight must be a positive finite number

ScreenChromeProvider calls it before rendering children, so a misconfigured screen fails fast at mount instead of producing NaN-driven animation glitches once the user starts scrolling.

mergeScreenChromeConfig

Resolves partial screen chrome overrides into a complete immutable configuration object, deep-merging colors and maskStops instead of replacing them wholesale.

const config = mergeScreenChromeConfig({ colors: { dark: { solid: 'black' } } });
// config.colors.dark.wash and config.colors.light stay at their defaults

A naive object spread ({ ...defaults, ...overrides }) replaces colors and maskStops outright when either key is present in overrides, because spread only merges at the top level: overriding one color scheme's solid value would silently delete the untouched scheme along with every other field of the touched one, and overriding one mask edge's stops would delete the untouched edge entirely. mergeScreenChromeConfig merges colors.light, colors.dark, maskStops.top, and maskStops.bottom independently so a caller can override exactly one nested field without having to restate everything else. Six spec cases pin this: scalar overrides, per-scheme color merges that preserve the sibling scheme, per-edge mask-stop merges that preserve the sibling edge, immutability of the source defaults and the caller's own overrides object, and non-sharing of nested mask-stop objects between the merged result and its inputs.

mergeRefs

Fans one instance out to several refs, so a chrome scroll ref can be attached alongside consumer and local refs.

import { useCollapsibleHeaderScroll } from '@rnw-community/react-native-collapsible-header';
import { mergeRefs } from '@rnw-community/react-native-screen-chrome';

const AnimatedKeyboardAwareScrollView = Animated.createAnimatedComponent(KeyboardAwareScrollView);
const { scrollRef } = useCollapsibleHeaderScroll();
const localRef = useRef<KeyboardAwareScrollView>(null);

<AnimatedKeyboardAwareScrollView ref={mergeRefs(scrollRef, localRef, props.ref)} />;

Object refs receive the instance on .current, function refs are called with it, and null/undefined entries in the ref list are skipped. Detach is handled both ways React 19 can request it: the returned cleanup runs each child ref's own cleanup where one was returned and clears the rest, and calling the merged callback with null directly forwards that null to every ref — so no ref is left holding a stale instance and no child cleanup is dropped.

ScrollContentInsetMode

Selects whether chrome content padding replaces consumer padding or stacks on top of it.

const mode: ScrollContentInsetMode = 'additive';

The two members mirror the two utilities: 'replace' resolves padding through mergeScrollContentInset and 'additive' through addScrollContentInset. ScreenChromeScrollView consumes it as contentInsetMode.

addScrollContentInset

Stacks safe-area and chrome content padding on top of consumer padding, leaving horizontal padding untouched.

const contentContainerStyle = addScrollContentInset(insets, 96, 48, { paddingTop: 8 });
// paddingTop: insets.top + 96 + 8, paddingBottom: insets.bottom + 48 — no paddingLeft/paddingRight injected

The additive counterpart to mergeScrollContentInset: consumer paddingTop/paddingBottom are summed with the computed insets instead of replacing them, so migrating from an additive layout keeps the computed inset. Only numeric, explicit paddingTop/paddingBottom participate. padding/paddingVertical shorthands are carried through untouched and do not join the sum. Percentage paddingTop/paddingBottom are not supported: both edges always resolve to the computed numeric total, so a percentage there is replaced rather than preserved — the same contract the consumer implementations this util replaces have shipped with.

mergeScrollContentInset

Prepends safe-area and chrome content padding while preserving consumer styles after generated padding.

const contentContainerStyle = mergeScrollContentInset(insets, 96, 48, { paddingHorizontal: 16 });

Public utilities

addScrollContentInset stacks safe-area and chrome padding on top of consumer padding — the additive counterpart to mergeScrollContentInset, selected on the scroll view via contentInsetMode="additive". mergeRefs fans one instance out to several refs so the chrome scroll ref can coexist with consumer refs. mergeScrollContentInset composes safe-area and caller chrome padding while retaining consumer styles last; consumer padding is applied after the generated padding, so a consumer paddingTop replaces the computed safe-area+chrome inset rather than adding to it — in 'replace' mode only; 'additive' mode stacks instead. useScrollFadeStyle creates a clamped opacity style from the collapsible-header provider scroll value and therefore requires a ScreenChromeProvider ancestor.

License

This library is licensed under the MIT License.