AHAP-style haptics (transient + continuous) on top of Nitro Modules — the core functions are UI-thread friendly ('worklet').
Complete flexibility over iOS haptics with synchronous UI-thread execution.
iOS only (Core Haptics). Android support could be added in the future, but it's currently unplanned.
npm i @renegades/react-native-tickle react-native-nitro-modules
duration) and optionally curves (automation over time).events[] and curves[] are separateOn iOS Core Haptics, a pattern is made of two different building blocks:
relativeTime, with base intensity/sharpness.They're separate because they're different object types in Core Haptics (events vs parameter curves) and they serve different jobs: events define the structure, curves define the modulation. You often combine both in one pattern.
|
|
Try out **Haptix Studio** to see what's possible with haptics. Play with continuous patterns and real-time updates, then export your creations directly into code. [Download on Test Flight](https://testflight.apple.com/join/HJn3mbb3) |
Wrap your app inside HapticProvider. This initializes the engine and automatically destroys it when the app goes to background.
import { HapticProvider } from '@renegades/react-native-tickle';
export function App() {
return <HapticProvider>{/* {Rest of your app} */}</HapticProvider>;
}
startHaptic(events, curves) (transient + continuous patterns)Play a transient:
import { startHaptic } from '@renegades/react-native-tickle';
startHaptic(
[
{
type: 'transient',
relativeTime: 0,
parameters: [
{ type: 'intensity', value: 1 },
{ type: 'sharpness', value: 0.5 },
],
},
],
[]
);
Play a continuous pattern (events + curves together):
import { startHaptic } from '@renegades/react-native-tickle';
startHaptic(
[
{
type: 'continuous',
relativeTime: 0,
duration: 1200,
parameters: [
{ type: 'intensity', value: 0.2 },
{ type: 'sharpness', value: 0.5 },
],
},
],
[
{
type: 'intensity',
relativeTime: 0,
controlPoints: [
{ relativeTime: 0, value: 0.2 },
{ relativeTime: 600, value: 1.0 },
{ relativeTime: 1200, value: 0.2 },
],
},
]
);
Combine transient + continuous in one pattern:
import { startHaptic } from '@renegades/react-native-tickle';
startHaptic(
[
{
type: 'transient',
relativeTime: 0,
parameters: [
{ type: 'intensity', value: 1 },
{ type: 'sharpness', value: 0.8 },
],
},
{
type: 'continuous',
relativeTime: 50,
duration: 800,
parameters: [
{ type: 'intensity', value: 0.2 },
{ type: 'sharpness', value: 0.4 },
],
},
],
[]
);
Use this when you can't predefine a pattern. You start the player, update it in real time, then stop it.
import { useContinuousPlayer } from '@renegades/react-native-tickle';
function MyComponent() {
const { start, stop, update } = useContinuousPlayer('my-player', 1.0, 0.5);
const gesture = Gesture.Pan()
.onBegin(() => {
start();
})
.onUpdate((e) => {
update(e.translationY / 100, 0.5);
})
.onEnd(() => {
stop();
});
}
If users can navigate away from a screen while haptics are still playing, call stopAllHaptics() in your cleanup to stop them.
import { stopAllHaptics } from '@renegades/react-native-tickle';
import { useEffect } from 'react';
export function SomeScreen() {
useEffect(() => () => stopAllHaptics(), []);
return null;
}
Disable haptics globally for users who prefer no haptic feedback. The setting is persisted across app restarts. When disabled, all haptic calls become no-ops.
import { useHapticsEnabled } from '@renegades/react-native-tickle';
function SettingsScreen() {
const [hapticsEnabled, setHapticsEnabled] = useHapticsEnabled();
return <Switch value={hapticsEnabled} onValueChange={setHapticsEnabled} />;
}
While the main purpose of this package is to support AHAP-style patterns (transient + continuous haptics with curves), system haptics are also available for completeness. These are simple, predefined OS-level feedback types that don't require pattern definitions. These methods are also UI-thread friendly.
import {
triggerImpact,
triggerNotification,
triggerSelection,
} from '@renegades/react-native-tickle';
// Impact feedback - simulates a physical collision
triggerImpact('light'); // 'light' | 'medium' | 'heavy' | 'soft' | 'rigid'
// Notification feedback - for alerts and status updates
triggerNotification('success'); // 'success' | 'warning' | 'error'
// Selection feedback - for picker wheels and toggles
triggerSelection();
For fine-grained control over the haptic API, you can opt out of the managed hooks and call the underlying functions directly:
initializeEngine() / destroyEngine() instead of HapticProvidersetHapticsEnabled() / getHapticsEnabled() instead of useHapticsEnabled()createContinuousPlayer() / startContinuousPlayer() / updateContinuousPlayer() / stopContinuousPlayer() / destroyContinuousPlayer() instead of useContinuousPlayer()| Function | Purpose |
|---|---|
HapticProvider |
Component that initializes engine; destroys on background |
useHapticEngine() |
Hook to manage engine lifecycle (used internally by provider) |
initializeEngine() / destroyEngine() |
Manual engine lifecycle |
startHaptic(events, curves) |
Play a pattern (transient + continuous events, optional curves) |
stopAllHaptics() |
Stop any running haptics (useful on unmount/navigation) |
useHapticsEnabled() |
Hook for reactive haptics enabled state |
setHapticsEnabled(enabled) / getHapticsEnabled() |
Manual enable/disable toggle (persisted) |
useContinuousPlayer(playerId, initialIntensity, initialSharpness) |
Hook to manage a continuous player lifecycle |
createContinuousPlayer(playerId, initialIntensity, initialSharpness) |
Create a continuous player with given ID |
startContinuousPlayer(playerId) / stopContinuousPlayer(playerId) |
Start/stop continuous playback for player |
updateContinuousPlayer(playerId, intensityControl, sharpnessControl) |
Update intensity/sharpness for player |
destroyContinuousPlayer(playerId) |
Destroy player and release resources |
triggerImpact(style) |
Trigger impact feedback (collision simulation) |
triggerNotification(type) |
Trigger notification feedback (alerts/status) |
triggerSelection() |
Trigger selection feedback (pickers/toggles) |
| Type | Values |
|---|---|
HapticParameterType |
'intensity' \| 'sharpness' |
HapticImpactStyle |
'light' \| 'medium' \| 'heavy' \| 'soft' \| 'rigid' |
HapticNotificationType |
'success' \| 'warning' \| 'error' |
HapticEventParameter |
Type |
|---|---|
type |
HapticParameterType |
value |
number |
HapticEvent (discriminated union)
Transient:
type: 'transient'
relativeTime: numberparameters: HapticEventParameter[]
Continuous:
type: 'continuous'relativeTime: numberduration: numberparameters: HapticEventParameter[]HapticCurveControlPoint |
Type |
|---|---|
relativeTime |
number |
value |
number |
HapticCurve |
Type |
|---|---|
type |
HapticCurveType |
relativeTime |
number |
controlPoints |
HapticCurveControlPoint[] |
In CoreHaptics, CHHapticParameterCurve uses hapticIntensityControl and hapticSharpnessControl — these are pattern-level multipliers, not per-event modifiers. Any curve you define will multiply the intensity/sharpness of all events playing at that moment, including transients.
Example problem:
startHaptic(
[
{ type: 'continuous', relativeTime: 0, duration: 2000, parameters: [...] },
{ type: 'transient', relativeTime: 1000, parameters: [{ type: 'intensity', value: 1.0 }, ...] },
],
[
{ type: 'intensity', relativeTime: 0, controlPoints: [
{ relativeTime: 0, value: 1.0 },
{ relativeTime: 1000, value: 0.3 }, // At t=1000ms, intensity control = 0.3
{ relativeTime: 2000, value: 0.3 },
]},
]
);
The transient at t=1000ms has base intensity 1.0, but the curve sets intensityControl=0.3 at that moment. Effective intensity: 1.0 × 0.3 = 0.3. The transient feels weaker than expected.
Workaround: Play continuous and transient events in separate startHaptic() calls:
// Continuous with curves
startHaptic(continuousEvents, curves);
// Transients without curves (separate pattern)
startHaptic(transientEvents, []);
Each call creates an isolated pattern/player — curves from one won't affect events in the other.
Note: The library automatically resets control values to
1.0at the end of each continuous event, so transients after a continuous event finishes are not affected. This limitation only applies to transients during a continuous event with active curves.
Shoutout to Jai for kicking off the Swift implementation 🙌
MIT
$ claude mcp add react-native-tickle \
-- python -m otcore.mcp_server <graph>