About this animation
A signature pad with paced ink replay and a folding paper preview, with configurable strokes, stroke width, palette, and fonts.
How it works
- A Gesture Handler pan owns only the pad. One
strokeStateshared record holds the active normalized points on UI; React receives a completed stroke once at pen-up. System cancellation or leaving the pad discards the unfinished stroke. - Each point retains its global millisecond timestamp and optional pressure. Without stylus pressure, a three-sample velocity average estimates pressure. Width is clamped to the configured range. Resizing preserves normalized positions and captured pressure.
- A single Skia outline has separate contours for every stroke. A time-based
replayProgressreveals partial segments and respects pen-up gaps; replay never emits data or confirmation callbacks. - Undo removes one complete stroke. Clear removes all strokes. Confirmation sends captured points, not converted drawing paths, once per confirmation cycle.
- One fold progress scales the clipped paper into a 104 × 64 thumbnail over 380 ms; the Lucide check draws over the final 160 ms. Reduced motion changes immediately to the finished state.
- A typed-name alternative has its own input and callback. A typed confirmation shows the name instead of ink and never converts text to strokes.
Install
Copy this entire folder, excluding demo/, into your project.
npx expo install @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-workletsThe host must provide GestureHandlerRootView. For web, load CanvasKit before importing this component using your app's Skia web bootstrap (LoadSkiaWeb or WithSkiaWeb), and serve the matching canvaskit.wasm. No safe-area provider is required by the component; the host owns insets and keyboard avoidance.
Usage
import { SignatureCaptureReplay } from './signature-capture-replay';
<SignatureCaptureReplay
onConfirm={(strokes) => saveDrawing(strokes)}
onConfirmTypedName={(name) => saveName(name)}
/>Controlled data and presentation knobs:
const handle = useRef<SignatureCaptureReplayHandle>(null);
const [strokes, setStrokes] = useState<readonly SignatureStroke[]>([]);
const [signed, setSigned] = useState(false);
<SignatureCaptureReplay
ref={handle}
strokes={strokes}
onStrokesChange={setStrokes}
signed={signed}
onSignedChange={setSigned}
colorScheme="dark"
strokeWidth={{ min: 1.5, max: 4 }}
motion={{ durationScale: 0.85, glowOpacity: 0.2 }}
palette={{ ink: '#F0D8AF' }}
labels={{ confirm: 'Save drawing', signed: 'Drawing saved' }}
/>;
// Imports: useRef/useState from React, and the handle/stroke types from this folder.
handle.current?.replay(2);Props
| Prop | Default | Purpose |
|---|---|---|
strokes / defaultStrokes | uncontrolled / [] | Readonly strokes { id, points: { x, y, t, pressure? }[] }. Coordinates are in [0,1]; timestamps are global nonnegative monotonic milliseconds. |
onStrokesChange | — | One callback for completed strokes and editing requests; replay does not call it. |
onConfirm | — | Receives the current captured data once when Confirm succeeds. |
signed / defaultSigned | uncontrolled / false | Controlled or initial confirmation state. |
onSignedChange | — | Requests confirmation or reset. |
typedName, onTypedNameChange | uncontrolled empty name | Independent controlled text alternative. |
onConfirmTypedName | — | Receives the trimmed nonempty name, never calls onConfirm. |
strokeWidth | { min: 1.5, max: 4 } | Width range; sanitized to 0.5–12 points with max ≥ min. |
labels | neutral English | pad, hint, undo, clear, replay, confirm, signed, empty, limitReached, useTypedName, typedName, confirmTypedName. All visible component copy is replaceable. |
disabled | false | Blocks drawing and all actions except reset(). |
colorScheme | 'auto' | 'light', 'dark', or system theme. |
palette | Paper / Night | Partial override of all semantic tokens plus ink, fiber, highlight, shadow. |
fontFamily, titleFontFamily, monoFontFamily | system | Control/body, typed preview, and pad-label fonts. |
motion | { durationScale: 1, glowOpacity: 0.25 } | Duration clamp 0.5–2; halo clamp 0–0.3. Nonfinite values use defaults. |
padHeight | 220 | Pad height, clamped to 120–300 points. |
scrollEnabled | true | Internal vertical scroll; set false when an outer host owns scrolling. |
style | — | Root override applied last; root defaults to flex:1. |
testID, accessibilityLabel | 'signature-capture-replay', — | Root semantics and stable IDs: -pad, -undo, -clear, -replay, -confirm, -typed-toggle, -typed-input, -typed-confirm. |
Handle and controlled behavior
setStrokes(strokes), undo(), clear(), replay(speed = 1), confirm(), reset(). Replay speed is clamped to 0.1–20. reset() clears ink, the typed input, and confirmation, including while disabled. Controlled values always win: methods request changes via their callback once and do not bypass the parent. The parent should accept or reject requests synchronously. Set signed={false} after an accepted confirmation to open a new confirmation cycle. Signed content is read-only until reset or external state changes.
Imports discard entire invalid strokes: empty/duplicate IDs, empty point lists, nonfinite or out-of-range coordinates/pressure, negative or decreasing timestamps. Unknown IDs are ordinary new IDs. The total budget is 4096 points. Oversized imports are evenly sampled while preserving their endpoints and timestamps; later strokes are skipped when no capacity remains. Live capture retains every accepted sample until the budget, then stops accepting samples. Undo or Clear frees capacity. Confirmation returns precisely this accepted data. Input objects are cloned rather than mutated. A canceled stroke consumes no budget.
Every real pen-up gap is retained. The demo's own three strokes use pauses below 300 ms and imports partial point sets through the public handle from 500–1700 ms, replays at 1800 ms, confirms at 2400 ms, and resets at 4300 ms. The five-second loop exists only in the demo and stops on real touch, backgrounding, reduced motion, and unmount.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, React 19.2.3, Reanimated 4.7.1, Worklets 0.13.0, Gesture Handler 3.3, and Skia 2.14.0. Uses modules bundled in Expo Go. No native folders or configuration changes are included.
No hardware FPS or legal-signature claim is made. Raster density and image download budgets: N/A; there are no raster assets.
Accessibility
All controls expose names, button roles, disabled states, minimum 48-point targets, and keyboard focus styling. Text scales with the OS. A text input and confirmation action let screen-reader and keyboard users finish without drawing. One announcement follows confirmation; ink updates do not announce per frame. Reduced motion is read initially and subscribed to at runtime: replay becomes immediate, the fold finishes immediately, and unfinished gestures are discarded to the last complete data. The internal vertical scroll makes short embedded hosts usable; the demo disables it and uses a single outer scroll, with a shorter pad on compact screens; drawing owns only the pad, with scroll available outside it. The host should provide keyboard avoidance when using the typed alternative.
Why this one
Ink remembers both pace and width. Replay includes the pauses between strokes, Undo removes a complete pen-down sequence, and the final miniature keeps ink clipped to its own paper. The typed path is an explicit alternative with separate data, rather than an imitation of screen-reader handwriting.
Original implementation and demo points. Icons: Lucide (ISC), copied exactly from the repository kit. Demo fonts: Geist, Geist Mono, Instrument Serif (OFL), injected by the host demo; the component ships no font files. This is a drawing interaction, without PDF export or electronic/legal signature certification.




