torch-haptics

expo
ios
npm
react-native
typescript
2026-03-26
torch-haptics

I published torch-haptics on npm today: an open-source Expo module that wraps Apple Core Haptics for React Native apps on iOS, with a TypeScript-first API. Source and issues live on GitHub.

The need showed up at work on Turf, a live prediction social experience for sports fans: you predict outcomes during live games and can win prizes. When a moment during live play needs to grab attention, we wanted haptics that feel intentional, not a generic buzz, so it registers without leaning only on more visual noise. Expo gives you great ergonomics, but wiring real Core Haptics from TypeScript was still something we would have treated as a one-off native project each time. That friction turned into a small library: implement the native surface once, shape patterns and players from JS, and reuse it anywhere we need the same level of control.

When the package went live I wrote a thread on X with more launch context and links:

Under the hood, the package is a standard Expo module (create-expo-module) with Swift on iOS using CHHapticEngine, pattern conversion, advanced players, and AHAP JSON parsed and played from the bridge. The JS side goes through Expo’s native module wiring (requireNativeModule), with the main entry points in HapticsEngine and helpers in TorchHapticsHelpers.ts. Failures surface with console.error so consumers stay in control of logging.

What you can do from TypeScript

The surface area is intentionally small. Here is the shape in practice (types for patterns, AHAP payloads, dynamic parameter IDs, and events live in src/TorchHaptics.types.ts):

import { HapticsEngine } from "torch-haptics";
import type { HapticPattern } from "torch-haptics";
 
await HapticsEngine.initialize();
if (!HapticsEngine.supportsHaptics()) {
  return;
}
 
const pattern: HapticPattern = {
  Pattern: [
    {
      Event: {
        Time: 0,
        EventType: "HapticTransient",
        EventParameters: [
          { ParameterID: "HapticIntensity", ParameterValue: 0.5 },
        ],
      },
    },
  ],
};
 
// One-shot patterns (transients, continuous segments, and related event types)
await HapticsEngine.play(pattern);
 
// Advanced player: dynamic intensity, sharpness, and lifecycle
const player = await HapticsEngine.createPlayer(pattern);
await player.start();
await player.sendParameter("HapticIntensityControl", 0.8);
await player.stop();
await player.destroy();
 
// AHAP: JSON string or object, including looping / continuous designs
const ahapJson = '{"Version":1,"Pattern":[]}'; // placeholder; real AHAP JSON is usually exported from authoring tools
await HapticsEngine.playAHAP(ahapJson);
const ahapPlayer = await HapticsEngine.createPlayerFromAHAP(ahapJson);
 
await HapticsEngine.stop();

See TorchHaptics.types.ts for every event type, parameter ID, and AHAP shape.

Expo and iOS reality (read this before you install)

This is iOS-only: the module config targets Apple platforms, not Android or web. Do not import it unconditionally in a universal app, or you risk native load errors on other platforms. Guard usage with Platform.OS === "ios", lazy imports, or an equivalent pattern so Android and web never touch the native module by accident:

import { Platform } from "react-native";
 
if (Platform.OS !== "ios") {
  return;
}
 
// Safe to load torch-haptics from here (or use a lazy import before calling in).

It also does not run in Expo Go, because it ships custom native code. You need a development build (expo run:ios, Xcode, or EAS Build).

If you are wrestling with the same thing, I would love for you to pull it from npm, read the README, and tell me how it goes. It is my first npm package out in the open, so a nudge on GitHub, whether that is an issue or a PR, actually helps me tighten it up.