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-native-keyboard

v0.1.1

Published

Native hardware keyboard events for React Native 0.73.6 and newer

Readme

react-native-native-keyboard

Generic, native hardware keyboard input for React Native >=0.73.6 (iOS + Android). This is a bare React Native package with automatic native linking. Expo is not a dependency; native Expo development builds may consume it as a normal RN library (Expo Go cannot load custom native modules).

The library reports physical key codes and native key changes, not a text-editing API and not a synthesizer-specific MIDI mapper. It does not use an invisible first-responder view, consume key input, or swizzle UIWindow.sendEvent.

Install

pnpm add react-native-native-keyboard
cd ios && pod install

Requires rebuilding the app. Both normal and isolated pnpm installs are supported (including `node-linker=isolated`): Android Gradle resolves `react-native` and its matching `@react-native/codegen` from the consuming app's dependency tree, not from the library's development checkout. No `node-linker=hoisted` workaround or manual `@react-native/codegen` dependency is required for this library.

On iOS, the capture implementation requires iOS 14+ GameController GCKeyboard; the CocoaPods deployment floor stays at 13.4 for compatibility with RN 0.73.6.

Xcode Node resolution

The example project's two Bundle React Native code and images phases use example/ios/scripts/bundle-react-native.sh. They do not assume node is in the restricted PATH of Xcode.app launched from the GUI. The resolver first honors a valid NODE_BINARY from the standard .xcode.env / .xcode.env.local files, then checks installed NVM versions and other supported Node managers, rejecting stale paths and Node versions older than 18. It never pins a developer's username or a particular Node version.

If Xcode reports that Node is missing, run example/ios/scripts/resolve-node.sh from the repository root to see the selected executable. You do not need to edit the example's Xcode Build Phases manually.

Declarative Android capture

For reliable physical-keyboard letters even while a TextInput inside your app is focused, wrap its content in the native KeyboardCaptureProvider. No MainActivity changes, root-view replacement, Accessibility permission, overlay or injected sibling view are needed. The provider is an ordinary focus-aware parent in your own React Native tree.

import {KeyboardCaptureProvider} from 'react-native-native-keyboard';

export default function App() {
  return (
    <KeyboardCaptureProvider>
      <NavigationContainer>
        <AppScreens />
      </NavigationContainer>
    </KeyboardCaptureProvider>
  );
}

The provider has flex: 1 by default and accepts an optional style. It never consumes keyboard events, intercepts touches, or makes itself clickable. It can hold ordinary Android keyboard focus while no descendant has it (enabled by default) and yields to focused TextInputs and buttons. No invisible sibling, hierarchy reparenting or Accessibility permission. Like any extra native parent, it participates in layout, so test custom absolute positioning and responder interactions if your application relies on them. A focused descendant sends the native event through this parent before the IME. The existing transparent Activity Window.Callback remains a best-effort post-IME fallback whenever no provider is on the focused view's ancestor path.

All existing TextInputs remain mounted and editable. Raw key observation does not decide where text goes: when a user focuses an input, typing still edits that field. To intentionally transfer keyboard focus back to the capture surface (for example when pressing Start in a game), increment the optional focusRequestId prop:

function KeyboardPlayScreen() {
  const [focusRequestId, setFocusRequestId] = React.useState(0);
  return (
    <KeyboardCaptureProvider focusRequestId={focusRequestId}>
      <Button title="Start / return to game input"
        onPress={() => setFocusRequestId(id => id + 1)} />
      <GameAndNormalTextInputs />
    </KeyboardCaptureProvider>
  );
}

Set claimFocusWhenEmpty={false} if the hosting app manages focus itself. When enabled (the default), the provider is a normal focus fallback, not a focus trap; child TextInputs and buttons still receive focus normally.

React Native Modal renders in a separate Android Dialog window, so wrap modal content with another declarative provider:

<Modal visible={visible} onRequestClose={close}>
  <KeyboardCaptureProvider>
    <ModalContent />
  </KeyboardCaptureProvider>
</Modal>

The same applies to other independently hosted native windows. Without a provider on the focused view's ancestor path, Android's IME may swallow normal letters before the Activity/window fallback sees them. System-reserved shortcuts and keys delivered outside this app are not guaranteed.

On iOS the provider renders its children without an extra native view; GameController handles ordinary physical keyboard capture independently.

Public API

import { NativeKeyboard } from 'react-native-native-keyboard';

const first = NativeKeyboard.addListener(event => {
  if (event.type === 'down' && !event.repeat) {
    console.log('press', event.code, event.deviceId);
  } else if (event.type === 'up' || event.type === 'cancel') {
    console.log('release', event.code);
  }
});
const independent = NativeKeyboard.addListener(event => {
  console.log('second listener', event.nativeCode);
});
const devices = NativeKeyboard.addConnectionListener(event => {
  console.log('connected', event.connected, event.deviceId);
});

// No singleton hook. Removing one listener leaves others alive.
first.remove();
console.log(await NativeKeyboard.isConnected());
console.log(await NativeKeyboard.getPressedKeys());
NativeKeyboard.releaseAll();
independent.remove();
devices.remove();

Keyboard event:

interface HardwareKeyEvent {
  type: 'down' | 'up' | 'cancel';
  code: string; // DOM-style physical key code, or 'Unidentified'
  nativeCode: number; // original platform key identifier
  repeat: boolean; // another DOWN while held
  timestampMs: number; // monotonic native timestamp, not UNIX time
  nativeEmittedAtMs?: number; // optional native emission timestamp
  modifiers: {shift: boolean; ctrl: boolean; alt: boolean; meta: boolean};
  scanCode?: number; // Android only
  deviceId?: number; // Android only, transient
  capturePhase?: 'pre-ime' | 'window' | 'cancellation'; // Android diagnostic
}

The native pressed snapshot is independently queryable. The library sends explicit cancellation for observed held keys on focus loss, app suspension, disconnect or releaseAll. On Android, cancellation logically ends a held key even if the physical switch is still down: remaining OS repeat events and the eventual key-up from that same physical hold are ignored. A genuine new press after release is accepted normally, including when the old up event was swallowed by a focus change. For musical instruments, treat cancel like note-up to avoid stuck notes.

Native architecture

iOS: GameController GCKeyboard.coalescedKeyboard and its keyChangedHandler. Normal UIKit text input retains focus. The underlying API coalesces multiple physical keyboards; per-keyboard device IDs are unavailable. On disconnect, the library conservatively cancels all held keys. It chains a previously installed GameController key handler and restores that previous handler if no other native code replaced ours in the meantime.

Android: A declarative native ViewGroup observes dispatchKeyEventPreIme() on its focused descendant's dispatch path, before the Android IME. The normal Window.Callback.dispatchKeyEvent() observer remains a post-IME fallback. A bounded event fingerprint cache deduplicates events observed in both stages. The provider is non-clickable, does not intercept touches, and does not alter Activity/root initialization. It can own normal keyboard focus only when no child has it, while remaining an ancestor of focused TextInputs and buttons. InputManager observes keyboard attachment changes. Held keys are keyed by device ID and physical key code and released on lifecycle changes. The native module and provider communicate through a weak, React-context-scoped registry.

The normalized physical-key mapper intentionally emits 'Unidentified' for unknown codes rather than falsely assuming platform-specific numbers correspond. Native codes are preserved. Physical code does not encode typed characters or keyboard layout.

Real platform limits

Hardware key rollover and OS-reserved key combinations can prevent the OS from delivering a complete event stream. No user-space library can reconstruct events it never sees. Native-to-JS event delivery also has scheduling jitter; instrument the actual keyboard-to-synth latency before deciding whether a separate native audio input sink is warranted.

GCKeyboard supports iOS14+, but actual keyboard and TextInput interactions still require validation on physical iPhones. On Android, an app without a provider has best-effort post-IME key capture and may miss printable keys when a TextInput/IME is focused. A provider only covers focused descendants in its own Android window, including TextInputs; React Native Modal needs its own provider. If a third party replaces the Activity Window.Callback without forwarding, the fallback may miss events. A foreground Activity is required. Android accessibility key filtering is not installed or requested.

Validation

pnpm install
pnpm typescript
pnpm test
pnpm android:rn073  # real RN 0.73.6 example: legacy + New Architecture
pnpm android:modern # isolated RN 0.83 New Architecture build
pnpm --dir example android

The example is pinned to RN 0.73.6. It now provides a 16-step guided QA session (13 core tests + 3 optional tests), per-step expectations and manual verdicts, live pressed-key reconstruction, focus/modal tests, app-background recovery, anomaly timestamps, persistent checkpoints, and one-file JSON report export via the device share sheet.

The npm package includes a device-testing guide at `docs/QA_TESTING.md` and an optional AI-assistant integration skill at `skills/react-native-native-keyboard/SKILL.md`. The example app, JSON report exporter and its local test configuration are not shipped with the npm package. The library itself does not record or upload a key log.

Real chord, rollover, focus and performance behavior must be checked with a physical keyboard. Compilation alone cannot certify device behavior.

License

MIT.

Example-only performance profiling

The bundled QA example can estimate native callback-to-JS delivery times on real hardware. It uses clock samples from its own native KeyboardQAStorage.getNativeUptimeMs() module and a bounded timing monitor, without involving the production library in React screen updates.

The cross-platform keyboard event now includes the optional nativeEmittedAtMs monotonic timestamp, sampled shortly before the native module emits to React Native. This shares the clock origin of the existing timestampMs on its platform and is diagnostic only. It does not claim hardware-switch-to-audio-output latency.

In example/QA_TESTING.md, use LIVE (250 ms) versus OFF (capture only) to isolate the additional QA React rendering from the native-to-JS event pipeline. No TextInputs are disabled, no keyboard events are consumed, and all raw key records remain exportable. The test schema remains backwards compatible; older JSON exports simply lack new stage.performance fields.

The QA app avoids per-key React state updates, maintains only a bounded recent-event preview, defers full analysis until a step finishes, and reduces full-report checkpoint writes during heavy event streams while still saving on completion and background. This is an example UI optimization rather than a semantic change to the keyboard-capture API.