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-signature-ink

v1.0.0

Published

True-native signature capture for React Native. PencilKit on iOS, hand-tuned velocity-Bezier on Android. Fabric-first, zero Skia.

Readme

react-native-signature-ink

True-native signature capture for React Native. Zero Skia, zero JS canvas, zero WebView. Buttery-smooth strokes, instant exports, and a clean imperative API — all powered by the platform's own ink engine.

Demo

| iOS | Android | | :---: | :---: | | iOS demo | Android demo |

Why this library

Most signature libraries on RN either render in JS (slow, jittery) or pull in Skia (large bundle, extra runtime, fights you on layout). This one is fully native on both sides:

  • Native rendering, native feel. Strokes are drawn by the OS's own ink pipeline — pressure-aware, sub-frame smooth, identical to what users get in the system Notes app.
  • Tiny footprint. No Skia, no Reanimated, no WebView, no third-party native deps. Pure Swift on iOS, pure Kotlin on Android.
  • Fabric-first. Built for the New Architecture from day one: codegen specs, view recycling-safe, deterministic prop diffing.
  • Real exports. PNG / JPEG / SVG, base64 / file URI / system clipboard / photo library, plus round-trippable raw stroke data with timestamps.
  • Drop-in DX. One <SignatureInk /> component, a typed imperative ref, sensible defaults. No setup beyond pod install.

Features

  • True native rendering on both platforms
  • Built-in toolbar (undo / redo / clear / copy) with configurable layout
  • PNG / JPEG / SVG export — base64, file URI, photo library, system clipboard
  • Replay animation with configurable speed
  • Round-trippable stroke data (getStrokeData / setStrokeData)
  • Apple Pencil exclusivity (iOS) / stylus-only mode (Android)
  • PencilKit system tool picker (iOS)
  • Configurable baseline (solid / dashed / dotted, width, offset, color)
  • Transparent canvas + dark/light theming hooks
  • Fabric-correct view recycling — no state leaks across screens, modals, lists
  • Density-independent units everywhere (pen widths render the same physical size on every device)

Installation

yarn add react-native-signature-ink
# or
npm install react-native-signature-ink

iOS

cd ios && pod install

If you plan to use saveToPhotoLibrary, add the permission key to your host app's Info.plist:

<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save your signature to your photo library.</string>

Android

No extra setup needed on API 29+. To support saveToPhotoLibrary on API ≤ 28, add the legacy storage permission to your host AndroidManifest.xml:

<uses-permission
  android:name="android.permission.WRITE_EXTERNAL_STORAGE"
  android:maxSdkVersion="28" />

The library bundles its own FileProvider for clipboard support, so you don't need to declare one yourself.

Requirements

  • React Native 0.75+ (New Architecture / Fabric enabled).
  • iOS 13+.
  • Android API 24+.

Quick start

import React, { useRef } from 'react';
import { Button, View } from 'react-native';
import { SignatureInk, type SignatureInkHandle } from 'react-native-signature-ink';

export function MySignaturePad() {
  const ref = useRef<SignatureInkHandle>(null);

  return (
    <View style={{ flex: 1 }}>
      <SignatureInk
        ref={ref}
        style={{ flex: 1 }}
        showBaseline
        showToolbar
        penColor="#111"
        onEnd={() => console.log('user lifted the pen')}
      />

      <Button
        title="Export"
        onPress={async () => {
          const base64 = await ref.current?.toBase64({ format: 'png', trim: true });
          console.log(base64?.slice(0, 64));
        }}
      />
    </View>
  );
}

Props

All props are optional. Defaults are documented inline in src/types.ts.

Pen

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | penColor | ColorValue | #111 | Captured literally on iOS (dark-mode auto-inversion disabled). | | penMinWidth | number | 1 | Width at the fastest pen velocity (pt on iOS, dp on Android). | | penMaxWidth | number | 3 | Width at the slowest pen velocity. Same units. | | velocityFilterWeight | number | 0.7 | Android only. 0..1 smoother weight. |

Canvas & baseline

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | backgroundColor | ColorValue | transparent | Pair with a light penColor for dark themes. | | showBaseline | boolean | false | Show the signing line. | | baselineColor | ColorValue | system gray @ 50% | | | baselineStyle | 'solid' \| 'dashed' \| 'dotted' | 'dashed' | | | baselineWidth | number | 0 | 0 = per-style auto value; any positive value overrides. | | baselineOffsetFromBottom | number | 8 (iOS) / 16 (Android) | Honoured only when the toolbar is hidden; otherwise the baseline auto-anchors to the toolbar edge. |

Input policy

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | pencilOnly | boolean | false | iOS: Apple Pencil only. Android: TOOL_TYPE_STYLUS only. |

Toolbar

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | showToolbar | boolean | false | Render the built-in native toolbar. | | toolbarPosition | 'top' \| 'bottom' | 'bottom' | | | toolbarButtons | ('undo' \| 'redo' \| 'clear' \| 'copy')[] | all four | Order is preserved. | | toolbarBackgroundColor | ColorValue | transparent | | | toolbarTintColor | ColorValue | platform accent | Tints SF Symbols (iOS) / vector drawables (Android). | | toolbarHeight | number | 44 (iOS) / 48 (Android) | | | toolbarIconSpacing | number | 8 | Horizontal gap between buttons. |

iOS-only

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | showToolPicker | boolean | false | Attach PencilKit's system tool picker (PKToolPicker). | | defaultInkType | 'pen' \| 'pencil' \| 'marker' \| 'monoline' \| 'fountainPen' \| 'watercolor' \| 'crayon' | 'pen' | iOS 14+ for the last four. |

Events

| Prop | Type | Fires | | --- | --- | --- | | onBegin | () => void | Finger / pencil down. | | onEnd | () => void | Finger / pencil up. | | onChange | (e: { isEmpty, strokeCount }) => void | Any drawing change. | | onReplayProgress | (e: { progress: number }) => void | Per-frame while replay() runs. | | onToolbarAction | (e: { action: 'undo' \| ... }) => void | After a toolbar button is tapped. |

Imperative API

Available via ref. All async methods return a Promise.

| Method | Returns | Notes | | --- | --- | --- | | clear() | void | Reversible via undo(). | | undo() / redo() | void | No-op when the respective stack is empty. | | copyToClipboard() | void | PNG into the system clipboard. | | isEmpty() | Promise<boolean> | | | toBase64(opts?) | Promise<string> | Raw base64, no data: prefix. | | toFile(opts?) | Promise<string> | file:// URI in the app temp dir. | | toSvg() | Promise<string> | SVG document string. | | getStrokeData() | Promise<StrokeData> | JSON-serializable strokes (with timestamps + pressure on iOS). | | setStrokeData(data) | void | Replace canvas contents. | | replay(opts?) | void | Animate existing strokes. | | saveToPhotoLibrary(opts?) | Promise<{ granted, uri? }> | iOS prompts the permission UI on first use. |

opts for image methods: { format?: 'png' | 'jpeg', quality?: number, trim?: boolean }.

Guides

Exports & clipboard

// Base64 (no `data:` prefix; raw payload).
const png = await ref.current?.toBase64({ format: 'png', trim: true });

// File URI in the app's temporary directory.
const fileUri = await ref.current?.toFile({ format: 'jpeg', quality: 0.85 });

// SVG with embedded paths.
const svg = await ref.current?.toSvg();

// PNG into the system clipboard. On Android this goes through the
// library's bundled FileProvider — no setup required on the host app.
ref.current?.copyToClipboard();

trim: true crops to the strokes' bounding box (plus a 2pt anti-alias inset). Defaults to false for toBase64 / toFile / toSvg, true for copyToClipboard and saveToPhotoLibrary.

Saving to the photo library

const result = await ref.current?.saveToPhotoLibrary({ format: 'png' });
if (!result.granted) {
  // iOS user denied the "Add Photos" prompt.
}
  • iOS: prompts the system "Add Photos" permission UI the first time. The host app must declare NSPhotoLibraryAddUsageDescription in Info.plist or iOS will crash the process.
  • Android: writes into Pictures/Signatures/ via MediaStore. API 29+ needs no runtime permission; API ≤ 28 requires WRITE_EXTERNAL_STORAGE. The promise resolves with the inserted content:// URI.

Stroke data round-trip

const data = await ref.current?.getStrokeData();
// ... persist, transmit, edit ...
ref.current?.setStrokeData(data);

The format is StrokePoint[][]. Every point has { x, y, t }; iOS additionally captures pressure, azimuth, altitude, and per-point size. Unknown fields are ignored on setStrokeData, so payloads round-trip cleanly across platforms.

Replay animation

ref.current?.replay({ speed: 1.5 }); // 1.5× natural pace

Speed is clamped to a minimum of 0.05. Any new stroke (or another replay() call) cancels the running animation.

Theming (dark / light)

The canvas defaults to transparent — the parent view's background shows through. For a dark theme:

<SignatureInk
  backgroundColor="#0c0c0c"
  penColor="#ffffff"
  baselineColor="rgba(255,255,255,0.4)"
  toolbarTintColor="#ffffff"
/>

iOS pins the underlying PKCanvasView to a light trait collection so user-set ink colors render literally (no PencilKit dark-mode auto-inversion surprises).

Apple Pencil-only

<SignatureInk pencilOnly />

On iOS, finger touches are silently dropped (PKCanvasViewDrawingPolicy.pencilOnly). On Android, only events with MotionEvent.TOOL_TYPE_STYLUS are accepted.

PencilKit tool picker (iOS)

<SignatureInk showToolPicker defaultInkType="fountainPen" />

Attaches the system PKToolPicker so the user can pick ink type, color, width, and switch between pen / eraser. Silently a no-op on Android. Tool-picker state is reset on view recycling so it never leaks across screens.

Architecture

The library is split along the codegen line: a TypeScript Fabric spec, two thin host wrappers, and one self-contained native rendering surface per platform.

                              ┌────────────────────────────────────┐
                              │  src/SignatureInk.tsx              │
                              │  • Promise/request-id back-channel │
                              │  • Public types (./types.ts)       │
                              └────────────────┬───────────────────┘
                                               │
                              ┌────────────────▼───────────────────┐
                              │  src/SignatureInkViewNativeComponent.ts (codegen) │
                              └─────────────┬──────────────────────┘
                                            │
                        ┌───────────────────┴────────────────────┐
                        ▼                                        ▼
       ┌────────────────────────────────┐          ┌─────────────────────────────────┐
       │ ios/SignatureInkView.mm        │          │ android/.../SignatureInkView.kt │
       │ (Fabric host, prop diff)       │          │ (Fabric host, synchronous layout)│
       └──────────────┬─────────────────┘          └──────────────┬──────────────────┘
                      ▼                                            ▼
       ┌────────────────────────────────┐          ┌─────────────────────────────────┐
       │ ios/SignatureInkSurface.swift  │          │ android/.../SignatureCanvasView │
       │ • PKCanvasView (PencilKit)     │          │ • Velocity-Bezier ink algorithm │
       │ • PKToolPicker                 │          │   (port of gcacace/warting)     │
       │ • PKDrawing.image(...) export  │          │ • Offscreen Bitmap → PNG/JPEG   │
       └────────────────────────────────┘          └─────────────────────────────────┘

iOS

  • Strokes are rendered by PKCanvasView, Apple's first-party ink engine — same one used by Notes and Markup. Pressure, tilt, and azimuth all flow through unchanged.
  • Exports go through PKDrawing.image(from:scale:), forced into a light trait collection so dark-mode hosts don't auto-invert ink in the output.
  • The Fabric host (ios/SignatureInkView.mm) does per-prop diffing and forwards to a Swift surface (ios/SignatureInkSurface.swift). The Obj-C++ ↔ Swift split keeps PencilKit types out of the Obj-C++ header (PencilKit isn't visible there).
  • View recycling is explicitly handled: every @objc public var is reset to its declared default in prepareForReuse, the PKCanvasView is replaced (not just .drawing = …), and the PKToolPicker is force-detached so it doesn't reappear on the next screen.

Android

  • Strokes are rendered by a hand-tuned velocity-Bezier algorithm (port of gcacace via warting) drawing into an offscreen Bitmap. Width tapers with pen speed; the bitmap doubles as the export source so PNG/JPEG/SVG are instant.
  • Pen widths, baseline width, and baselineOffsetFromBottom are stored in dp internally and converted to raw pixels at every draw site — so a penMaxWidth={3} renders at the same physical thickness across 1×/2×/3× densities and matches iOS visually.
  • Layout is performed synchronously on prop change (applyChildLayout() measures and positions the canvas + toolbar children directly), because Fabric on Android silently swallows requestLayout() calls from native descendants.
  • Clipboard exports go through a bundled FileProvider so content:// URIs can be shared cross-process without FileUriExposedException.

JavaScript

  • The codegen spec (src/SignatureInkViewNativeComponent.ts) is the single source of truth for props, commands, and event payload shapes — both platforms generate their Fabric glue from it.
  • The high-level wrapper (src/SignatureInk.tsx) owns a request-id ⇄ Promise map: each async command stamps a unique id, the native side echoes it back on the generic onResult event, and the wrapper resolves the matching pending Promise. One generic event channel keeps the codegen surface narrow.

For known gotchas, build commands, and contribution conventions, see AGENTS.md and CONTRIBUTING.md.

Contributing

License

MIT