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-system

v0.0.5

Published

Screen-level behavior orchestration for React Native.

Readme

react-native-screen-system

License: MIT React Native TypeScript Peer Dep

Behavior-first screen infrastructure for React Native.

Stop rewriting keyboard, focus, safe-area, and sticky-action logic on every screen. react-native-screen-system gives your team a set of small, composable primitives that wire together automatically — so you ship screens faster and keep them consistent.

Your keyboard will never hide a CTA button again.
Tab to the next field. Every time. No exceptions.
Safe area padding: set it once, forget it exists.
Loading · error · empty — three less if statements per screen.



Table of Contents


Why Use It

Use this package when your app keeps running into problems like:

  • inputs getting hidden behind the keyboard
  • form fields needing reliable next and previous focus behavior
  • bottom CTA bars needing to stay reachable on small devices
  • long forms needing to scroll the focused input into view
  • screen state handling repeating across loading, error, empty, and content flows
  • safe-area padding being manually reimplemented on every screen

This package is not a design system or navigation framework. It focuses on the interaction layer underneath your UI:

  • keyboard-aware screen layout
  • safe-area-aware spacing
  • focus order and input submit behavior
  • scroll-to-focused-input behavior
  • sticky bottom actions
  • screen state orchestration

Good Fit Use Cases

  • Sign in, sign up, OTP, forgot password, and recovery flows
  • Checkout, shipping, billing, payment, and address entry screens
  • Profile edit and account settings screens
  • Onboarding and multi-step form flows
  • Support and feedback forms
  • Internal tools with many input-heavy screens
  • Search, filter, and results screens with loading, empty, and error states

Install

npm install react-native-screen-system react-native-safe-area-context

Peer dependencies required:

| Package | Version | | --- | --- | | react | >=18.0.0 | | react-native | >=0.72.0 | | react-native-safe-area-context | >=4.0.0 |


Setup

Wrap your app once at the root. ScreenSystemProvider sets up shared context for all screens below it.

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

export function App() {
  return (
    <SafeAreaProvider>
      <ScreenSystemProvider>
        <RootApp />
      </ScreenSystemProvider>
    </SafeAreaProvider>
  );
}

Quick Example

A complete login screen — keyboard-aware layout, focus order, and sticky CTA — in under 40 lines:

import { Button, TextInput } from 'react-native';
import {
  ScreenContainer,
  ScreenScrollView,
  StickyActionBar,
  useFocusableField,
} from 'react-native-screen-system';

export function LoginScreen() {
  const email = useFocusableField({ id: 'email', order: 1, submitBehavior: 'next' });
  const password = useFocusableField({ id: 'password', order: 2, submitBehavior: 'blur' });

  return (
    <ScreenContainer keyboardAware includeBottomInset>
      <ScreenScrollView contentContainerStyle={{ padding: 16 }}>
        <TextInput
          ref={email.ref}
          placeholder="Email"
          onFocus={email.onFocus}
          onSubmitEditing={email.onSubmitEditing}
        />
        <TextInput
          ref={password.ref}
          placeholder="Password"
          secureTextEntry
          onFocus={password.onFocus}
          onSubmitEditing={password.onSubmitEditing}
        />
      </ScreenScrollView>

      <StickyActionBar divider>
        <Button title="Continue" onPress={() => {}} />
      </StickyActionBar>
    </ScreenContainer>
  );
}



Architecture

ScreenSystemProvider sets up three contexts internally — FocusControllerProvider, ScrollCoordinatorProvider, and ScreenSystemContext — so every component and hook shares the same state without any manual wiring.

useKeyboardInsets works standalone and does not require the provider.


API Reference

ScreenSystemProvider

App-level defaults. Override these once instead of repeating per screen.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | keyboardVerticalOffset | number | 0 | Base offset subtracted from keyboard spacing across the app | | actionBarBottomGap | number | 12 | Default extra bottom gap for sticky action bars | | defaultKeyboardBehavior | 'padding' \| 'margin' \| 'none' | 'padding' | Default keyboard behavior for ScreenContainer | | defaultActionBarKeyboardBehavior | 'padding' \| 'margin' \| 'none' | 'padding' | Default keyboard behavior for StickyActionBar |


ScreenContainer

Screen wrapper with keyboard and inset-aware spacing. Renders a View with flex: 1.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | keyboardAware | boolean | false | Turns on keyboard-aware bottom spacing | | keyboardBehavior | 'padding' \| 'margin' \| 'none' | 'padding' | Controls how keyboard space is applied | | keyboardVerticalOffset | number | — | Per-screen keyboard offset override | | keyboardInsetOverride | number | — | Manual keyboard inset override | | keyboardInsetAdjustment | number | 0 | Fine-tunes the computed keyboard inset | | includeTopInset | boolean | false | Adds safe area top inset | | includeBottomInset | boolean | true | Adds safe area bottom inset | | topInsetOverride | number | — | Manual top inset override | | bottomInsetOverride | number | — | Manual bottom inset override | | extraTopInset | number | 0 | Adds extra top spacing on top of inset | | extraBottomInset | number | 0 | Adds extra bottom spacing on top of inset |

Accepts all ViewProps.


ScreenScrollView

A ScrollView wrapper that registers with the scroll coordinator so focused inputs are scrolled into view automatically.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | scrollSystemId | string | 'default-scroll-view' | Unique id for the registered scroll view | | autoRegister | boolean | true | Automatically registers with the scroll coordinator | | enabled | boolean | true | Enables or disables scroll-to-field for this view | | scrollToFocusedInputOffset | number | 24 | Extra space left above the focused input | | preventNegativeScrollOffset | boolean | true | Prevents scroll overshoot above the top | | fallbackTopInset | number | 0 | Extra top inset for the manual scroll fallback |

Accepts all ScrollViewProps. Supports forwardRef.


StickyActionBar

Bottom action area that stays visible above the keyboard and respects safe area.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | keyboardAware | boolean | true | Moves the action bar above the keyboard | | keyboardBehavior | 'padding' \| 'margin' \| 'none' | 'padding' | Controls how keyboard space is applied | | safeAreaAware | boolean | true | Includes bottom safe area spacing | | keyboardVerticalOffset | number | — | Per-bar keyboard offset override | | keyboardInsetOverride | number | — | Manual keyboard inset override | | safeAreaInsetOverride | number | — | Manual bottom safe area override | | bottomOffset | number | 12 | Extra bottom gap under the action bar content | | divider | boolean | false | Shows a hairline divider above the action bar | | dividerColor | string | '#D1D5DB' | Custom divider color |

Accepts all ViewProps.


ScreenStateView

Renders different content based on screen state. Accepts either an explicit state prop or individual boolean flags.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | state | ScreenStatus | — | Explicit state override ('loading' \| 'error' \| 'empty' \| 'content') | | loading | boolean | false | Loading flag (ignored when state is set) | | error | unknown \| null | null | Error value (ignored when state is set) | | empty | boolean | false | Empty flag (ignored when state is set) | | renderLoading | () => ReactNode | — | Custom loading renderer | | renderError | (error) => ReactNode | — | Custom error renderer | | renderEmpty | () => ReactNode | — | Custom empty renderer | | renderContent | () => ReactNode | — | Custom content renderer (falls back to children) | | children | ReactNode | — | Default content, used when no renderContent is provided |


useFocusableField

Wires a TextInput into the focus system. Returns a ref, event handlers, and imperative focus helpers.

Options:

| Option | Type | Default | Description | | --- | --- | --- | --- | | id | string | — | Required. Stable field id used by the focus system | | order | number | — | Required. Order used for next / previous navigation | | nextId | string | — | Explicit next field id | | previousId | string | — | Explicit previous field id | | targetId | string | — | Explicit submit target field id | | disabled | boolean | false | Excludes the field from navigation when true | | submitBehavior | 'next' \| 'previous' \| 'target' \| 'blur' \| 'none' | auto-detected | What happens when the user submits the field | | blurOnSubmit | boolean | false | Blur the field on submit (used in auto-detection fallback) | | autoScrollOnFocus | boolean | true | Scrolls the field into view when it receives focus | | scrollToFocusedInputOffset | number | — | Extra space above the focused field during scroll | | preventNegativeScrollOffset | boolean | — | Prevents scroll overshoot above the top | | onFocus | () => void | — | Called when the field receives focus | | onSubmitEditing | (event) => void | — | Called when the field's submit button is tapped |

submitBehavior auto-detection (when not explicitly set):

  • Has targetId or nextId'target'
  • Has previousId'previous'
  • blurOnSubmit is true'blur'
  • Otherwise → 'next'

Returns:

| Field | Type | Description | | --- | --- | --- | | ref | RefObject<TextInput \| null> | Attach to the TextInput | | onFocus | () => void | Attach to TextInput.onFocus | | onSubmitEditing | (event) => void | Attach to TextInput.onSubmitEditing | | focusNext | () => boolean | Imperatively focuses the next field | | focusPrevious | () => boolean | Imperatively focuses the previous field | | focusSelf | () => boolean | Imperatively focuses this field |


useFocusController

Imperative focus controller. Useful for form validation (focus first invalid field) and custom UI interactions.

| Field | Type | Description | | --- | --- | --- | | registerField | (field) => () => void | Registers a field manually; returns an unregister function | | focusField | (id) => boolean | Focuses a field by id | | focusNext | (currentId) => boolean | Focuses the next registered field after currentId | | focusPrevious | (currentId) => boolean | Focuses the previous registered field before currentId | | focusFirstInvalid | (invalidIds) => boolean | Focuses the first focusable field in invalidIds |

Must be used inside ScreenSystemProvider.


useScreenState

Converts boolean flags into a single ScreenStatus string. Priority: loadingerroremptycontent.

Options:

| Option | Type | Default | Description | | --- | --- | --- | --- | | loading | boolean | false | Signals loading state | | error | unknown \| null | null | Signals error state when truthy | | empty | boolean | false | Signals empty state |

Returns: 'loading' | 'error' | 'empty' | 'content'


useKeyboardInsets

Tracks keyboard visibility and height. Works standalone — no provider needed.

| Field | Type | Description | | --- | --- | --- | | visible | boolean | Whether the keyboard is currently visible | | keyboardHeight | number | Raw keyboard height from native events | | bottom | number | Keyboard bottom inset after safe area adjustment | | animationDuration | number | Keyboard animation duration (iOS only) |

Uses keyboardWillShow / keyboardWillHide on iOS and keyboardDidShow / keyboardDidHide on Android.


Customization

Global defaults via ScreenSystemProvider

Set once at the app root. All screens inherit these automatically.

<ScreenSystemProvider
  keyboardVerticalOffset={44}
  actionBarBottomGap={0}
  defaultKeyboardBehavior="padding"
  defaultActionBarKeyboardBehavior="padding"
>
  <RootApp />
</ScreenSystemProvider>

keyboardVerticalOffset is useful when your navigation header contributes extra height that affects keyboard offset calculations.

Per-screen keyboard behavior override

Each screen can override the global defaults:

<ScreenContainer
  keyboardAware
  keyboardBehavior="margin"
  keyboardVerticalOffset={60}
>
  {/* ... */}
</ScreenContainer>

Custom safe area handling

Use overrides when you manage safe area outside the component:

<ScreenContainer
  includeTopInset={false}
  includeBottomInset={false}
  topInsetOverride={0}
  bottomInsetOverride={0}
>
  {/* ... */}
</ScreenContainer>

Add extra spacing on top of the computed inset:

<ScreenContainer
  includeTopInset
  extraTopInset={8}
  includeBottomInset
  extraBottomInset={16}
>
  {/* ... */}
</ScreenContainer>

Explicit focus order with gaps

Leave gaps in the order sequence so you can insert fields later without renumbering:

const firstName = useFocusableField({ id: 'firstName', order: 10 });
const lastName  = useFocusableField({ id: 'lastName',  order: 20 });
const email     = useFocusableField({ id: 'email',     order: 30 });
const phone     = useFocusableField({ id: 'phone',     order: 40, submitBehavior: 'blur' });

Conditional fields

Mark a field as disabled to skip it in focus navigation without unmounting:

const altEmail = useFocusableField({
  id: 'altEmail',
  order: 35,
  disabled: !showAltEmail, // skipped when the field is hidden
});

Custom scroll offset per field

Override the global scroll offset for a specific field that needs more breathing room:

const longAnswer = useFocusableField({
  id: 'longAnswer',
  order: 50,
  scrollToFocusedInput
  Offset: 48,
});

Focusing the first invalid field on submit

Use useFocusController to drive form validation UX:

const { focusFirstInvalid } = useFocusController();

function handleSubmit() {
  const invalid = validate(); // returns string[] of invalid field ids
  if (invalid.length > 0) {
    focusFirstInvalid(invalid);
    return;
  }
  submit();
}

Screen state rendering

Wire loading / error / empty states without if-else chains:

function ProductScreen() {
  const { data, isLoading, error } = useProduct(id);

  return (
    <ScreenStateView
      loading={isLoading}
      error={error}
      empty={data?.items.length === 0}
      renderLoading={() => <Spinner />}
      renderError={(err) => <ErrorView error={err} />}
      renderEmpty={() => <EmptyState />}
    >
      <ProductList items={data.items} />
    </ScreenStateView>
  );
}

Examples

Full runnable examples are in example/:

| File | What it shows | | --- | --- | | ProviderAndLayoutExample.tsx | Provider setup and layout defaults | | FormFlowExample.tsx | Multi-step keyboard-aware form | | FocusControllerExample.tsx | Imperative focus control | | ScrollCoordinatorExample.tsx | Scroll coordinator with multiple inputs | | ScreenStatesExample.tsx | Loading / error / empty / content states | | KeyboardInsetsExample.tsx | Live keyboard inset inspection |

See example/README.md for the full breakdown.


Contributing

Contributions are welcome.

Suggested workflow:

  1. Fork the repository or create a feature branch.
  2. Make focused changes.
  3. Run npm run build and npm run typecheck.
  4. Update docs or examples when behavior changes.
  5. Open a pull request with a clear summary of the change.

See CONTRIBUTING.md for more detail.


Roadmap

See PLAN.md.


License

MIT. See LICENSE.