React Native SDK
The Pulsar React Native SDK (react-native-pulsar) exposes haptic feedback through three building blocks: Presets, usePatternComposer, and useRealtimeComposer. All preset functions and hook methods are worklet-compatible for use with Reanimated.
Requirements
Section titled “Requirements”- React Native 0.71+
- New Architecture enabled
Installation
Section titled “Installation”Latest available version: 1.6.1
npx expo install react-native-pulsarThen run prebuild to generate the native project files:
npx expo prebuildnpm install react-native-pulsar react-native-workletsPresets
Section titled “Presets”A collection of ready-to-use haptic patterns. All preset functions are worklet-compatible and can be called directly inside Reanimated worklets.
import { Presets } from 'react-native-pulsar';Built-in presets
Section titled “Built-in presets”| Method | Description |
|---|---|
afterglow() | A three-beat phrase that dissolves gently, ideal for soft endings or gradually quieting feedback. |
aftershock() | A firm opening that settles calmly, ideal for transitions needing a strong start and a gentle finish. |
alarm() | Relentless and urgent, best for critical errors or emergencies that require immediate attention. |
anvil() | The full weight of a massive collision, conveys sheer physical force and momentum. |
applause() | A growing wave of appreciation, ideal for celebratory moments or social approval. |
Example
import { Presets } from 'react-native-pulsar';
Presets.hammer();System presets
Section titled “System presets”Common system presets
Section titled “Common system presets”Platform-specific haptic feedback styles, common across iOS and Android.
| Method | Description |
|---|---|
impactHeavy() | UIImpactFeedbackGenerator.heavy |
impactLight() | UIImpactFeedbackGenerator.light |
impactMedium() | UIImpactFeedbackGenerator.medium |
impactRigid() | UIImpactFeedbackGenerator.rigid |
impactSoft() | UIImpactFeedbackGenerator.soft |
Example
import { Presets } from 'react-native-pulsar';import { Gesture } from 'react-native-gesture-handler';
const tap = Gesture.Tap().onEnd(() => { Presets.System.impactMedium();});Android-specific system presets
Section titled “Android-specific system presets”Additional system haptic feedback styles that are only available on Android.
| Method | Description |
|---|---|
calendarDate() | HapticFeedbackConstants.CALENDAR_DATE |
clockTick() | HapticFeedbackConstants.CLOCK_TICK |
confirm() | HapticFeedbackConstants.CONFIRM |
contextClick() | HapticFeedbackConstants.CONTEXT_CLICK |
dragCrossing() | HapticFeedbackConstants.DRAG_CROSSING |
Example
import { Presets } from 'react-native-pulsar';
Presets.System.Android.primitiveLowTick();usePatternComposer
Section titled “usePatternComposer”A React hook for composing and playing a custom Pattern. The pattern is parsed on mount and whenever it changes. Resources are released automatically on unmount.
const { play, parse, isParsed } = usePatternComposer(pattern);Parameters
Section titled “Parameters”pattern (optional)
Section titled “pattern (optional)”Type: Pattern
A haptic pattern to parse on mount. When provided, the pattern is parsed automatically and re-parsed whenever the value changes. Resources are released on unmount.
Returns
Section titled “Returns”play()
Section titled “play()”Plays the parsed pattern.
stop()
Section titled “stop()”Stops the active pattern.
parse(pattern: Pattern)
Section titled “parse(pattern: Pattern)”Parses and replaces the current pattern.
isParsed()
Section titled “isParsed()”Returns true if a pattern has been parsed and is ready to play.
Example
Section titled “Example”import { usePatternComposer } from 'react-native-pulsar';import { Gesture } from 'react-native-gesture-handler';
const pattern = { discretePattern: [ { time: 0, amplitude: 1, frequency: 0.5 }, { time: 100, amplitude: 0.5, frequency: 0.5 }, ], continuousPattern: { amplitude: [ { time: 0, value: 0 }, { time: 200, value: 1 }, { time: 400, value: 0 }, ], frequency: [ { time: 0, value: 0.3 }, { time: 400, value: 0.8 }, ], },};
function MyComponent() { const composer = usePatternComposer(pattern);
const tap = Gesture.Tap().onEnd(() => { composer.play(); });
return <GestureDetector gesture={tap}>...</GestureDetector>;}useRealtimeComposer
Section titled “useRealtimeComposer”A React hook for real-time haptic control. Provides live amplitude and frequency modulation, useful for gesture-driven or continuously evolving haptic experiences. Haptics stop automatically on unmount.
const { set, playDiscrete, stop, isActive } = useRealtimeComposer();Parameters
Section titled “Parameters”useRealtimeComposer doesn’t take any parameters.
Returns
Section titled “Returns”set(amplitude: number, frequency: number)
Section titled “set(amplitude: number, frequency: number)”Updates the ongoing haptic with new amplitude and frequency values.
playDiscrete(amplitude: number, frequency: number)
Section titled “playDiscrete(amplitude: number, frequency: number)”Plays a single discrete haptic event.
stop()
Section titled “stop()”Stops the active haptic.
isActive()
Section titled “isActive()”Returns true if a haptic is currently playing.
Example
Section titled “Example”import { useRealtimeComposer } from 'react-native-pulsar';import { Gesture } from 'react-native-gesture-handler';
function MyComponent() { const realtime = useRealtimeComposer();
const pan = Gesture.Pan() .onUpdate((e) => { const amplitude = Math.min(Math.abs(e.velocityY) / 1000, 1); realtime.set(amplitude, 0.5); }) .onEnd(() => { realtime.stop(); });
return <GestureDetector gesture={pan}>...</GestureDetector>;}useAdaptiveHaptics
Section titled “useAdaptiveHaptics”A React hook that plays haptics from a cross-platform AdaptivePreset. It automatically selects the correct configuration for the current platform (iOS or Android). Each platform can provide either a custom Pattern or a native preset function.
const { play } = useAdaptiveHaptics(preset: AdaptivePreset);Parameters
Section titled “Parameters”preset: AdaptivePreset
Section titled “preset: AdaptivePreset”An object with ios and android keys. Each value is either a Pattern object or a function that triggers a native preset directly.
type AdaptivePresetConfig = (() => void) | Pattern;
type AdaptivePreset = { ios: AdaptivePresetConfig; android: AdaptivePresetConfig;};Returns
Section titled “Returns”play()
Section titled “play()”Plays the haptic for the current platform. If the platform config is a function, it is called directly. If it is a Pattern, it is played via the pattern composer.
Example
Section titled “Example”import { useAdaptiveHaptics, Presets } from 'react-native-pulsar';
const adaptivePreset = { ios: Presets.Success, // native iOS preset function android: { // custom pattern for Android discretePattern: [ { time: 0, amplitude: 1, frequency: 0.5 }, { time: 150, amplitude: 0.6, frequency: 0.4 }, ], continuousPattern: { amplitude: [], frequency: [], }, },};
function MyComponent() { const haptics = useAdaptiveHaptics(adaptivePreset);
return ( <Button onPress={haptics.play} title="Tap me" /> );}Settings
Section titled “Settings”Global configuration for the Pulsar SDK. All methods are available on the Settings object.
import { Settings } from 'react-native-pulsar';| Method | Description |
|---|---|
Settings.enableHaptics(state: boolean) | Enable or disable all haptic feedback |
Settings.enableSound(state: boolean) | Enable or disable audio simulation |
Settings.enableCache(state: boolean) | Enable or disable preset caching |
Settings.clearCache() | Clear the preset cache |
Settings.preloadPresets(presetNames: string[]) | Preload presets by name for faster playback |
Settings.stopHaptics() | Stop all currently playing haptics |
Settings.shutDownEngine() | Shut down the haptic engine |
Settings.getHapticsSupportLevel() | Returns the device’s HapticSupport level |
Settings.forceHapticsSupportLevel(level) | (Android only) Override the detected support level. Intended for testing/debugging fallback behavior. |
Settings.enableImpulseCompositionMode(state: boolean) | (Android only) Enable or disable impulse composition mode |
Settings.setRealtimeComposerStrategy(strategy) | (Android only) Set the strategy used by the realtime composer |
Example
Section titled “Example”import { Settings } from 'react-native-pulsar';
// Preload frequently used presetsSettings.preloadPresets(['Fanfare', 'Explosion', 'Heartbeat']);
// Disable haptics temporarilySettings.enableHaptics(false);
// Check device supportconst support = Settings.getHapticsSupportLevel();HapticSupport
Section titled “HapticSupport”The haptic capability level of the current device, returned by Settings.getHapticsSupportLevel().
import { HapticSupport } from 'react-native-pulsar';enum HapticSupport { NO_SUPPORT = 0, LIMITED_SUPPORT = 1, STANDARD_SUPPORT = 2, ADVANCED_SUPPORT = 3,}On Android, Settings.forceHapticsSupportLevel(level) uses these exact numeric enum values when selecting the fallback path. If you force a mode while validating custom patterns, make sure you pass the exported HapticSupport enum rather than app-local aliases.
Example
Section titled “Example”import { Settings, HapticSupport, Presets } from 'react-native-pulsar';
const support = Settings.getHapticsSupportLevel();
if (support >= HapticSupport.STANDARD_SUPPORT) { Presets.dogBark();}Pattern
Section titled “Pattern”Describes a complete haptic pattern with discrete pulses and continuous envelope curves.
type Pattern = { discretePattern: Array<{ time: number; // Milliseconds from pattern start amplitude: number; // Intensity (0-1) frequency: number; // Sharpness (0-1) }>; continuousPattern: { amplitude: Array<{ time: number; // Milliseconds from pattern start value: number; // Amplitude value (0-1) }>; frequency: Array<{ time: number; // Milliseconds from pattern start value: number; // Frequency value (0-1) }>; };};Use discretePattern for distinct taps and impacts. Use continuousPattern envelopes to shape a sustained haptic over time.
HapticSupport
Section titled “HapticSupport”enum HapticSupport { NO_SUPPORT = 0, LIMITED_SUPPORT = 1, STANDARD_SUPPORT = 2, ADVANCED_SUPPORT = 3,}RealtimeComposerStrategy
Section titled “RealtimeComposerStrategy”Android-only. Controls how useRealtimeComposer simulates continuous haptics, since Android has no native continuous haptic API. Pass one of these values to Settings.setRealtimeComposerStrategy().
import { RealtimeComposerStrategy } from 'react-native-pulsar';enum RealtimeComposerStrategy { ENVELOPE = 0, PRIMITIVE_TICK = 1, PRIMITIVE_COMPLEX = 2, ENVELOPE_WITH_DISCRETE_PRIMITIVES = 3,}| Value | Description |
|---|---|
ENVELOPE | Approximation based on the Envelope API. Allows control over amplitude and frequency, but the signal oscillates and can be unstable. Available on Android API 36+. |
PRIMITIVE_TICK | Approximation using the Composition API TICK primitive at varying intervals. Amplitude is controllable; frequency is simulated by the timing between ticks. Signal is discrete rather than continuous. |
PRIMITIVE_COMPLEX | Similar to PRIMITIVE_TICK, but uses multiple primitives depending on the requested frequency. |
ENVELOPE_WITH_DISCRETE_PRIMITIVES | Default. Hybrid strategy. Uses the Envelope API for continuous events (API 36+) and composition primitives for discrete events (API 33+). Best of both worlds when both event types are used. |
Example
import { Settings, RealtimeComposerStrategy } from 'react-native-pulsar';
Settings.setRealtimeComposerStrategy(RealtimeComposerStrategy.PRIMITIVE_COMPLEX);Testing with Jest
Section titled “Testing with Jest”react-native-pulsar is backed by a TurboModule and relies on react-native-worklets. In a Jest environment there is no native module to bind to, so importing the library directly throws (TurboModuleRegistry.getEnforcing('RNPulsar') cannot find the native module). To keep tests for components that use Pulsar running on Node, the package ships a ready-to-use mock.
Ready-to-use mock
Section titled “Ready-to-use mock”The mock lives at react-native-pulsar/jest-mock. It replaces the entire public API — Presets, Settings, the hooks, and the enums — with no-op jest.fn() implementations, so nothing tries to reach a native module. You don’t have to enumerate the 150+ presets yourself: every preset (at any nesting depth, e.g. Presets.System.Android.effectClick) is a jest.fn() you can assert on.
Wire it up with jest.mock, either per test file or globally for the whole suite.
Add this at the top of any test file that imports react-native-pulsar:
jest.mock('react-native-pulsar', () => require('react-native-pulsar/jest-mock'));Register the mock once in a Jest setup file so it applies to every test.
jest.mock('react-native-pulsar', () => require('react-native-pulsar/jest-mock'));Then reference that file from your Jest config:
module.exports = { preset: 'react-native', setupFiles: ['<rootDir>/jest.setup.js'],};Asserting that haptics fired
Section titled “Asserting that haptics fired”Because every export is a jest.fn(), you can assert that your component triggered the haptic you expect:
import { render, fireEvent } from '@testing-library/react-native';import { Presets } from 'react-native-pulsar';import LikeButton from '../LikeButton';
jest.mock('react-native-pulsar', () => require('react-native-pulsar/jest-mock'));
it('plays a success haptic when liked', () => { const { getByRole } = render(<LikeButton />);
fireEvent.press(getByRole('button'));
expect(Presets.System.notificationSuccess).toHaveBeenCalledTimes(1);});The hooks are mocked too. useRealtimeComposer, usePatternComposer, and useAdaptiveHaptics each return an object whose methods (play, stop, set, …) are jest.fn()s, so components that call them render without errors.
Customizing return values
Section titled “Customizing return values”The mock ships with sensible defaults — for example Settings.getHapticsSupportLevel() returns HapticSupport.ADVANCED_SUPPORT, and isActive() / isParsed() return false. Override any of them in a test when you need to exercise a different code path:
import { Settings, HapticSupport } from 'react-native-pulsar';
jest.mock('react-native-pulsar', () => require('react-native-pulsar/jest-mock'));
it('falls back when the device has no haptics', () => { (Settings.getHapticsSupportLevel as jest.Mock).mockReturnValue( HapticSupport.NO_SUPPORT );
// ...assert your fallback UI / behavior});Call jest.clearAllMocks() in afterEach (or set clearMocks: true in your Jest config) to reset call counts between tests.