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

v2.0.1

Published

Screen shell and keyboard-aware scrolling for React Native that behaves identically on iOS and Android. New Architecture ready.

Readme

react-native-screen-kit

npm license types

Screen shell and keyboard-aware scrolling for React Native that behaves identically on iOS and Android.

  • Zero runtime dependencies.
  • New Architecture native — Fabric, bridgeless. No legacy UIManager calls (which are silent no-ops under bridgeless).
  • Keyboard metrics normalized, so both platforms report the same number for the same visual state.
  • Verified on device, not just in unit tests — 140 unit tests plus 110 on-device assertions across both platforms.
npm install react-native-screen-kit react-native-safe-area-context
cd ios && pod install

Contents


Install & setup

Dependencies

Runtime dependencies: zero. The package has no dependencies field.

Peer dependencies — you install these:

| Package | Range | Why it's needed | |---|---|---| | react | >=17.0.0 | hooks | | react-native | >=0.70.0 | core | | react-native-safe-area-context | >=4.0.0 | ScreenWrapper reads safe-area insets from it |

Required provider

SafeAreaProvider must be above ScreenWrapper:

import { SafeAreaProvider } from 'react-native-safe-area-context';

export default function App() {
  return (
    <SafeAreaProvider>
      <RootNavigator />
    </SafeAreaProvider>
  );
}

Inside a <Modal> on Android you need a nested SafeAreaProvider — a Modal renders in its own Dialog window, which the root provider cannot see into:

<Modal visible={open} transparent statusBarTranslucent>
  <SafeAreaProvider>
    <SheetContent />
  </SafeAreaProvider>
</Modal>

Tested matrix

Stated plainly, because the peer range is wider than what has been verified:

| | Verified on | Not verified | |---|---|---| | React Native | 0.87.0 (New Architecture, bridgeless, edge-to-edge) | everything else in the peer range | | React | 19.2.3 | 17.x, 18.x | | react-native-safe-area-context | 5.9.0 | 4.x | | iOS | iPhone 17 Pro simulator, iOS 26.5 | real iOS hardware, iPad | | Android | one physical device, Android 15 / API 35, Gboard, edge-to-edge | other OEMs, older APIs, tablets, foldables | | Orientation | portrait | landscape / rotation | | Font scale | 0.85 | accessibility sizes 1.3–2.0 |

Below RN ~0.76 you are on the legacy renderer, where Fabric's DOM contains() is absent and subtree containment falls back to a geometric heuristic that has not been exercised on a device.


Quick start

import { ScreenWrapper } from 'react-native-screen-kit';

export function LoginScreen() {
  return (
    <ScreenWrapper statusBarColor="#0B1B3A" barStyle="light-content">
      <TextInput placeholder="Email" />
      <TextInput placeholder="Password" secureTextEntry />
      <Button title="Sign in" onPress={submit} />
    </ScreenWrapper>
  );
}

That gives you safe-area padding, a painted status bar strip, a scroll container, and a focused input that scrolls clear of the keyboard — the same on both platforms.


What "identical on both platforms" means

The platforms disagree about the keyboard in several concrete ways. Every one is normalized:

| | iOS reports | Android reports | This library reports | |---|---|---|---| | Keyboard height | full frame, including the home-indicator strip | imeInsets.bottom - systemBarInsets.bottom — navigation bar subtracted | how much of the window the keyboard covers, identical on both | | Height on hide | still the full height | 0 | 0 | | Floating / undocked iPad keyboard | a large height | n/a | 0 — it covers none of the window | | Animation duration | the real value (measured 383.3ms on iOS 26.5) | always 0 | the real value on iOS; 250ms on Android | | Advance warning | keyboardWillShow | none — did* events only | one KeyboardState lifecycle |

Height is derived from endCoordinates.screenY, not endCoordinates.height. That single choice removes the first three rows at once.

Measured on device, per input, keyboard-to-input gap configured at 16dp:

| | iOS | Android | |---|---|---| | form field alignment (7 fields) | 16.0dp | 16.0dp | | chat composer on keyboard | 0.0dp | 0.0dp | | modal comment composer | 0.0dp | 0.0dp | | bottom-anchored sheet | 0.0dp | 0.0dp |


Components

ScreenWrapper

Use case: wrap a whole screen. Safe area + status bar + scroll + keyboard in one shell.

Safe-area space is painted as ordinary layout views sized from useSafeAreaInsets, rather than relying on StatusBar.backgroundColor. That property is Android-only and became a deprecated no-op once React Native went edge-to-edge in 0.87, so painting a real view is the only way to get the same pixels on both platforms.

| Prop | Type | Default | Use case | |---|---|---|---| | children | ReactNode | — | screen content | | backgroundColor | string | '#FFFFFF' | screen background behind every layer | | statusBarColor | string | 'transparent' | tint the status-bar strip; match your header colour so they read as one block | | bottomBarColor | string | 'transparent' | tint the bottom safe-area strip | | backgroundComponent | ReactNode | — | full-bleed background image / gradient / doodle; runs under the status bar, pointerEvents: none | | barStyle | StatusBarStyle | 'dark-content' | 'light-content' on dark headers | | hideStatusBar | boolean | false | immersive / media screens | | edges | readonly SafeAreaEdge[] | ['top','bottom'] | which safe-area edges to pad; ['top'] for chat, [] for full-bleed, add 'left'/'right' for landscape notches | | headerComponent | ReactNode | — | fixed header above the scroll area; omit for no header | | footerComponent | ReactNode | — | fixed footer below the scroll area | | footerAvoidsKeyboard | boolean | false | lift the footer onto the keyboard (action bars, submit buttons) | | disableScroll | boolean | false | render a plain View — for screens with their own FlatList or chat layout | | isKeyboardScrollActive | boolean | true | false → plain ScrollView, no keyboard handling | | scrollViewProps | Partial<KeyboardAwareScrollViewProps> | — | anything you'd pass the scroll container | | keyboardDismissMode | 'none' \| 'on-drag' \| 'interactive' | 'none' | 'on-drag' = dismiss on scroll (symmetric); 'interactive' = iOS-only gesture | | style | StyleProp<ViewStyle> | — | outermost view | | contentStyle | StyleProp<ViewStyle> | — | the body area holding header / content / footer | | contentContainerStyle | StyleProp<ViewStyle> | — | forwarded to the scroll container | | testID | string | — | e2e | | isStatusBarActive | boolean | — | deprecated (1.x) — use edges |


KeyboardAwareScrollView

Use case: a scrolling form. Drop-in ScrollView replacement that scrolls the focused input clear of the keyboard.

Inherits every ScrollViewProps except automaticallyAdjustKeyboardInsets, which is forced off so iOS doesn't add its own inset on top. onScroll, onLayout, onContentSizeChange and onTouchEnd are forwarded to you, not swallowed.

| Prop | Type | Default | Use case | |---|---|---|---| | extraKeyboardSpace | number | 16 | breathing room between the input's bottom edge and the keyboard top | | reserveKeyboardSpace | boolean | true | pad the content so the last field is still reachable, extraKeyboardSpace included | | autoScrollToFocusedInput | boolean | true | turn off to handle scrolling yourself | | restoreScrollOnClose | boolean | false | return to the pre-keyboard offset on dismiss | | trackFocusChanges | boolean | true | follow focus moving between fields — taps and onSubmitEditing → next | | enableOnAndroid | boolean | true | false → defer entirely to windowSoftInputMode | | keyboardToolbarHeight | number | 0 | reserve extra dp for a custom bar above the keyboard | | onKeyboardChange | (m: KeyboardMetrics) => void | — | react to keyboard state in the parent | | innerRef | React.Ref | — | 1.x escape hatch; ref also works now |

Defaults it sets for you: keyboardShouldPersistTaps: 'handled', keyboardDismissMode: 'none', showsVerticalScrollIndicator: false, scrollEventThrottle: 16, contentContainerStyle: { flexGrow: 1 }.


KeyboardAvoidingContainer

Use case: chat and comment layouts — a list that fills the space plus a composer glued to it. The container shrinks by however much of it the keyboard covers, which is what keeps an inverted list anchored to the composer.

| Prop | Type | Default | Use case | |---|---|---|---| | children | ReactNode | — | list + composer | | extraSpace | number | 0 | extra gap kept below the content while open | | enabled | boolean | true | toggle at runtime | | onKeyboardChange | (m: KeyboardMetrics) => void | — | e.g. swap composer padding when the keyboard opens | | style | StyleProp<ViewStyle> | — | defaults to flex: 1 | | testID | string | — | e2e |

Must be flex-sized. A container sized by its content, or one at position: absolute; bottom: 0, cannot shrink out of the keyboard's way — lift those with KeyboardStickyView instead.

React Native's own KeyboardAvoidingView is not used internally because its behavior prop has to differ per platform (padding on iOS, height on Android) to look right — exactly the divergence this library exists to remove.


KeyboardStickyView

Use case: anything bottom-anchored that cannot shrink — action bars, absolutely positioned sheets, footers outside the scroll container.

Uses transform: translateY on an Animated.Value with useNativeDriver: true, so the animation runs on the UI thread and holds frame rate while the JS thread is busy. Animating bottom or height cannot use the native driver, which is the usual reason a sticky footer visibly lags the keyboard.

The lift distance is measured, not assumed: whatever already sits below the view (a bottom safe-area strip, a tab bar) absorbs part of the keyboard, and lifting by the full keyboard height would overshoot by exactly that much. Measurement happens on layout while the keyboard is down, so nothing is added to the open latency.

| Prop | Type | Default | Use case | |---|---|---|---| | children | ReactNode | — | the bar / sheet | | offset | number | 0 | extra dp to ignore on top of the auto-measured gap; normally unnecessary | | disableAutoOffset | boolean | false | skip self-measurement; offset becomes the whole adjustment | | enabled | boolean | true | toggle at runtime | | style | StyleProp<ViewStyle> | — | | | testID | string | — | e2e |


The three content modes

ScreenWrapper renders one of three containers, checked in this order:

| Setting | Container | Scrolls | Keyboard handling | scrollViewProps | |---|---|---|---|---| | (default) | KeyboardAwareScrollView | ✅ | ✅ auto-scroll + reserved space | ✅ | | isKeyboardScrollActive={false} | plain RN ScrollView | ✅ | ❌ | ✅ | | disableScroll | plain View | ❌ | ❌ | ❌ discarded |

disableScroll is checked first, so it wins over isKeyboardScrollActive.

The shell is identical in all three — safe-area strips, statusBarColor, edges, headerComponent, footerComponent, backgroundComponent. Only the middle container changes, and footerAvoidsKeyboard keeps working even with disableScroll.


Which component do I want?

| Your layout | Component | |---|---| | A scrolling form | KeyboardAwareScrollView, or ScreenWrapper for the whole screen | | A list plus a composer that must stay glued to it — chat, comments | KeyboardAvoidingContainer | | A bar anchored to the bottom that cannot shrink — action bar, absolute sheet | KeyboardStickyView | | A whole screen shell (safe area + status bar + scroll) | ScreenWrapper |


Hooks

| Hook | Returns | Use case | |---|---|---| | useKeyboard() | KeyboardMetrics | full normalized state; conditional layout and padding | | useIsKeyboardVisible() | boolean | show/hide UI while typing | | useKeyboardHeight() | number | normalized overlap in dp | | useAnimatedKeyboardOffset(opts?) | Animated.Value | native-driver value tracking the keyboard | | useResponsive() | ResponsiveMetrics | live, orientation-aware metrics and scaling |

const { height, screenY, duration, easing, state, isVisible } = useKeyboard();

useAnimatedKeyboardOffset({ offset?: number, enabled?: boolean }) is driven with useNativeDriver: true, so it must be consumed by a native-driver-compatible prop — transform: [{ translateY }] or opacity. Layout props such as height or paddingBottom cannot be driven natively and would push every frame through JS.

useResponsive() members

| Member | Type | Use case | |---|---|---| | width / height | number | live window dp — updates on rotation, fold, split-screen, Stage Manager | | isLandscape | boolean | orientation branching | | isTablet | boolean | shortest side ≥ 600dp (Android's sw600dp bucket) | | breakpoint | 'small' \| 'medium' \| 'large' \| 'xlarge' | layout buckets | | fontScale | number | OS accessibility font scale | | pixelDensity | number | asset selection | | scaleSize(n) | (number) => number | proportional spacing/sizes, clamped 0.85–1.4× | | scaleFont(n) | (number) => number | font sizes, clamped 0.85–1.25×; does not re-apply fontScale, React Native already does | | roundToPixel(n) | (number) => number | keeps hairline borders from vanishing | | select({ default, ... }) | generic | per-breakpoint values, falls back to the nearest smaller entry |

const r = useResponsive();
const columns = r.select({ default: 1, large: 2, xlarge: 3 });
const gutter  = r.scaleSize(16);

Types

type KeyboardState = 'closed' | 'opening' | 'open' | 'closing';

interface KeyboardMetrics {
  height: number;     // dp of the window the keyboard covers — normalized across platforms
  screenY: number;    // window-space Y of the keyboard's top edge
  duration: number;   // ms; real value on iOS, 250 substituted on Android
  easing: 'easeIn' | 'easeInEaseOut' | 'easeOut' | 'linear' | 'keyboard';
  state: KeyboardState;
  isVisible: boolean; // true when the keyboard covers any part of the window
}

type SafeAreaEdge = 'top' | 'bottom' | 'left' | 'right';
type Breakpoint   = 'small' | 'medium' | 'large' | 'xlarge';

Also exported: ResponsiveMetrics, ScreenWrapperProps, KeyboardAwareScrollViewProps, KeyboardStickyViewProps, KeyboardAvoidingContainerProps, ScrollViewInstance, Rect, ScrollAlignmentInput.


Advanced / low-level

Exported for building your own containers. Not needed for normal use.

| Export | Signature | Purpose | |---|---|---| | KeyboardSource | .getMetrics(), .subscribe(fn) | the single process-wide keyboard subscription; getMetrics() is synchronous and safe to read during render | | measureInWindow | (node) => Promise<Rect \| null> | Fabric-safe measurement; resolves null instead of hanging on unmounted nodes | | isDescendant | (container, node) => boolean \| null | exact subtree containment via Fabric's DOM contains(); null when undeterminable | | computeScrollTarget | (ScrollAlignmentInput) => number \| null | the pure scroll-alignment maths | | computeReservedSpace | (keyboardHeight, viewportBottomGap) => number | how much bottom space to reserve | | setBottomSafeAreaInset | (n: number) => void | feeds the Android fallback normalization path; ScreenWrapper calls it for you |


Workflows

1. Scrolling form

<ScreenWrapper
  statusBarColor="#0B1B3A"
  barStyle="light-content"
  headerComponent={<Header title="Profile" />}
  scrollViewProps={{ extraKeyboardSpace: 24, restoreScrollOnClose: true }}
>
  {fields}
</ScreenWrapper>

2. Chat (WhatsApp shape)

<ScreenWrapper edges={['top']} headerComponent={<ChatHeader />} disableScroll>
  <KeyboardAvoidingContainer>
    <FlatList inverted data={messages} renderItem={renderMessage} keyboardDismissMode="none" />
    <View style={{ paddingBottom: keyboard.isVisible ? 8 : insets.bottom + 8 }}>
      <TextInput multiline placeholder="Message" />
    </View>
  </KeyboardAvoidingContainer>
</ScreenWrapper>

The wrapper owns only the top edge; the composer supplies its own bottom padding, so no safe-area strip sits between it and the keyboard.

3. Comment sheet in a Modal (Instagram shape)

<Modal visible={open} transparent animationType="slide" statusBarTranslucent>
  <SafeAreaProvider>
    <View style={{ flex: 1, justifyContent: 'flex-end' }}>
      <Pressable style={StyleSheet.absoluteFill} onPress={close} />
      <View style={{ height: '72%', backgroundColor: '#fff' }}>
        <KeyboardAvoidingContainer>
          <FlatList data={comments} renderItem={renderComment} />
          <Composer />
        </KeyboardAvoidingContainer>
      </View>
    </View>
  </SafeAreaProvider>
</Modal>

Two keyboard-aware surfaces end up mounted at once — the feed's and the sheet's. TextInput.State.currentlyFocusedInput() is global, so KeyboardAwareScrollView verifies the focused input is in its own subtree before scrolling. Without that check, the feed behind the sheet would scroll to reveal a field it does not contain.

4. Pull to refresh

There is no dedicated prop — refreshControl is part of ScrollViewProps, which the scroll container inherits, so pass it through scrollViewProps:

const [refreshing, setRefreshing] = useState(false);

const refreshControl = useMemo(
  () => (
    <RefreshControl
      refreshing={refreshing}
      onRefresh={onRefresh}
      tintColor="#2F6FED"   // iOS
      colors={['#2F6FED']}  // Android
    />
  ),
  [refreshing, onRefresh],
);

<ScreenWrapper scrollViewProps={{ refreshControl }}>{content}</ScreenWrapper>

Works on the keyboard-aware container and on the plain ScrollView (isKeyboardScrollActive={false}) alike. It does not work with disableScroll, because that mode renders no scroll container at all — you'll get a dev warning if you try.

5. Sticky action bar

<ScreenWrapper
  footerAvoidsKeyboard
  footerComponent={<SubmitBar onPress={save} />}
>
  {fields}
</ScreenWrapper>

Or standalone, for an absolutely positioned bar:

<KeyboardStickyView style={{ position: 'absolute', left: 0, right: 0, bottom: 0 }}>
  <ReplyBar />
</KeyboardStickyView>

6. Background image or doodle

<ScreenWrapper
  backgroundComponent={<Image source={wallpaper} style={StyleSheet.absoluteFill} resizeMode="cover" />}
  statusBarColor="transparent"
  barStyle="light-content"
  headerComponent={<TransparentHeader />}
>
  {children}
</ScreenWrapper>

backgroundComponent renders full-bleed behind the safe-area strips, so it extends under the status bar rather than stopping at it, and is pointerEvents="none" so it cannot eat taps.

7. Coloured header

Give the header its own background and set statusBarColor to the same value, so the strip and the header read as one block:

<ScreenWrapper
  statusBarColor="#0B1B3A"
  barStyle="light-content"
  headerComponent={<View style={{ height: 52, backgroundColor: '#0B1B3A' }}>{/* … */}</View>}
>

8. No header

Omit headerComponent. Content begins directly under the top safe-area strip; nothing collapses and no space is reserved.

9. Custom safe-area edges

<ScreenWrapper edges={['top']} />                     // chat: composer owns the bottom
<ScreenWrapper edges={[]} />                          // full-bleed media
<ScreenWrapper edges={['top','bottom','left','right']} />  // landscape with notches

Behaviour notes

Scrolling never dismisses the keyboard, on either platform. keyboardDismissMode defaults to 'none'. Set 'on-drag' for dismiss-on-scroll — that mode is implemented in JavaScript, so it stays symmetric. 'interactive' is a UIKit feature with no Android equivalent and silently degrades to 'none' there, so choosing it is a deliberate decision to differ.

Taps are governed separately by keyboardShouldPersistTaps, defaulting to 'handled': tapping a button or another input keeps the keyboard and the child still receives the tap; tapping empty background dismisses it. Identical on both platforms.

Reserved space is applied instantly; only the scroll animates. Layout props cannot be driven natively, so animating paddingBottom would push every frame through JS. Changing the scrollable extent while the keyboard slides over the content is imperceptible, and the result is zero per-frame JS work — measured 0% jank across six keyboard cycles on iOS, 1.6% on Android.

Focus tracking costs nothing at rest. React Native exposes no global focus event, so the focused ref is sampled — per frame in a short burst after a touch, and on a 150ms heartbeat only while the keyboard is open. Sampling is a single property read; with the keyboard closed there is no work at all.

The focused input stays put when anything above it moves. Two separate cases, both handled: a headerComponent that grows or collapses when a field is focused (this resizes the scroll view, so onLayout is the only signal), and content changing height inside the scroller — a validation message, an async block, a multiline field growing. In both cases nothing about the keyboard or the focus has changed to announce it. The field is re-anchored on the next frame, without animating, because the correction cancels a layout shift that already happened.

The focused input stays put when your content changes. A validation message appearing above the field, an async block loading, a multiline field growing — all of these move the focused input while the keyboard is open, and nothing about the keyboard or the focus has changed to signal it. The scroll container watches its own content size and re-anchors the input on the next frame, without animating: the content above grew, and the view stays fixed on the field you are typing in. Content that grows below the input moves nothing and is left alone.

Alignment is idempotent. The offset used in the maths is derived from the content container's measured position, not from the last onScroll event, which lags and is simply wrong part-way through an animated scroll. Recomputing 200ms into a 300ms scroll therefore yields the identical destination, so the engine can run on every keyboard event, focus change, content-size change and layout, and still issue exactly one motion per real change.

The scroll maths is viewport-relative. Everything is measured in window coordinates and reduced to a delta, so a scroll view under a header or above a tab bar lands correctly. React Native's built-in scrollResponderScrollNativeHandleToKeyboard documents that it assumes the scroll view fills the screen — and on the New Architecture it is a no-op anyway, because it routes through the legacy tag-based UIManager.measureLayout.


Changelog

2.0.1

2.0.0 shipped a working keyboard engine. 2.0.1 makes it hold — it fixes every case where the focused input ended up somewhere other than where it was put, and closes three API gaps.

Keyboard alignment

  • Fixed: the focused input drifted down and stayed there whenever anything above it moved — a headerComponent that changes size on focus, a validation message, an async block, an autogrowing field. Nothing about the keyboard or the focus changes in those cases, so there was no signal the old code looked at, and the field silently ended up under the keyboard or behind a footerAvoidsKeyboard footer. Content changes are now caught via onContentSizeChange and header resizes via onLayout. Measured on device with a 66dp block inserted above the focused field: re-anchored within one frame on both platforms, back to exactly the requested 16dp.
  • Fixed: the focused input visibly rose and then settled back a few dp. The scroll offset was read from the last onScroll event, which lags by up to scrollEventThrottle ms and part-way through an animated scroll reports a position the content has already left — so every recomputation during an opening produced a different destination and fought the scroll already in flight. The offset is now derived from the content container's measured position, which makes the computation idempotent: recomputing mid-animation yields the identical destination. Verified on device at 1 motion, 0dp drift over 0 frames, on iOS and Android, for programmatic focus and for real taps.
  • Fixed: the last element could never get its extraKeyboardSpace gap. Reserved space covered only the keyboard overlap, so the final field's target was clamped and it landed flush against the keyboard — or flush against a keyboard-riding footer — while every other field got its 16dp.
  • Fixed: with footerAvoidsKeyboard, the focused input landed behind the footer. A footer that rides the keyboard covers the bottom of the scroll view once it lifts, so the scroller now treats the footer's top edge as the real visible bottom.
  • Fixed: KeyboardStickyView restarted a full-duration lift on every keyboard event. iOS emits keyboardWillShow, keyboardWillChangeFrame and keyboardDidShow for one opening, so the footer needed up to two durations to settle and landed 5-10dp short. Redundant restarts are now suppressed — measured at 0.0dp across three consecutive runs.
  • Fixed: a manual drag no longer competes with a pending alignment.

Layout

  • Fixed: ScreenWrapper never gave the scroll container flex: 1, so with both a header and a footer the ScrollView sized to its content and pushed the footer past the body's bottom edge.
  • Fixed: keyboardDismissMode was ignored when isKeyboardScrollActive={false} — the plain ScrollView branch never received it.

API

  • Added: MeasurableInstance is exported. It is the ref type for KeyboardStickyView and KeyboardAvoidingContainer, so refs to those components were previously impossible to type.
  • Added: KeyboardListener and KeyboardSourceApi are exported, for typing a KeyboardSource.subscribe callback.
  • Added: computeReservedSpace takes an optional third argument, extraSpace. Existing two-argument calls are unaffected.
  • Added: onScrollBeginDrag is forwarded rather than swallowed, alongside onScroll, onLayout, onContentSizeChange and onTouchEnd.
  • Removed: all default exports, including KeyboardAwareScrollViewDefault. Named exports only, on the package and on every module.
  • Changed: the entry point now declares its exports instead of re-exporting them, so "Go to Definition" lands on the manifest — where the whole API is visible — rather than jumping straight into an implementation file.

Other

  • Added: dev warning when disableScroll discards scrollViewProps — this silently killed refreshControl with no signal.
  • Added: API.md ships in the package — every export, prop table and type in one flat file.
  • Docs: pull-to-refresh and the three content modes are documented, in the README and llms.txt.
  • Tests: 13 test files to 19; 199 tests.

Migrating from 1.x

  • import { KeyboardAwareScrollView } from 'react-native-screen-kit' now works. In 1.x the entry point only re-exported ScreenWrapper, so that documented import resolved to undefined.
  • 1.x shipped raw untransformed JSX in dist/*.js (jsx: "react-native" preserves JSX), which broke Jest, plain Node and any non-Babel bundler. 2.x ships compiled CommonJS.
  • 1.x auto-scroll was a no-op on the New Architecture — it routed through UIManager.measureInWindow and UIManager.measureLayout, both soft-error no-ops under bridgeless.
  • isStatusBarActive still works, deprecated in favour of edges.
  • innerRef still works; ref is now forwarded too.
  • extraHeight / extraScrollHeight → single extraKeyboardSpace.
  • viewIsInsideTabBar removed — the overlap is measured, so a tab bar is handled automatically.
  • enableResetScrollToCoords / resetScrollToCoordsrestoreScrollOnClose.
  • keyboardDismissMode now defaults to 'none' on both platforms instead of 'on-drag' on Android. Pass keyboardDismissMode="on-drag" to keep the old Android behaviour — on both platforms.

Known limitations

Android footer lift is not frame-synced. Android emits no keyboardWillShow (React Native's own type declares duration: 0 as always true there), so KeyboardStickyView begins moving from keyboardDidShow. The final position is exact on both platforms — footer top lands on keyboardTop - footerHeight to the dp, verified on device — but whether the lift tracks the Android IME frame-for-frame is unverified: a useNativeDriver transform is invisible to measureInWindow on both platforms, so it cannot be measured from JavaScript. Establishing it needs frame capture, and frame-locking it needs a native WindowInsetsAnimationCompat.Callback module.

The tested matrix is narrow. See Tested matrix. Rotation, other Android OEMs, real iOS hardware, tablets, accessibility font scales and RTL are all unverified.

Android Modal needs a nested SafeAreaProvider. A constraint of react-native-safe-area-context, not something this library can work around.


LLM-readable reference

llms.txt ships inside the package with a condensed, machine-oriented reference — exact exports, prop signatures, decision rules, correct patterns and common mistakes.

node_modules/react-native-screen-kit/llms.txt

License

MIT