One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Haptics

Tactile feedback for selections, impacts, and outcomes on iOS and Android

One.Haptics plays the system’s tactile feedback: a light tick when people move through choices, a tap of a given weight when something lands, and a pattern for success, warning, or error. On iOS it is UIKit’s feedback generators; on Android it is the window’s performHapticFeedback, which needs no vibrate permission.

The cross-platform methods are named for what happened, so each platform plays its own feel for the moment. performAndroidHaptics also exposes Android’s named feedback patterns directly. Web calls do nothing.

Usage

import { Pressable, Text } from 'react-native'
import { One } from 'one'
function LikeButton({ liked, onToggle }: { liked: boolean; onToggle: () => void }) {
return (
<Pressable
onPress={() => {
One.Haptics.impact('light')
onToggle()
}}
>
<Text>{liked ? 'Liked' : 'Like'}</Text>
</Pressable>
)
}

The calls return nothing and never wait: feedback has no result to await, and awaiting would only delay the next one.

How it works

The JavaScript call. Each method is a synchronous JSI call into a Nitro module. The style or type is checked first; an unknown value throws a TypeError before anything reaches native code. Native code then hops to the main thread, where both platforms require feedback to run, and returns immediately.

iOS. One keeps one generator per style for the life of the app: UISelectionFeedbackGenerator, a UIImpactFeedbackGenerator for each impact style, and UINotificationFeedbackGenerator. Each call prepares the generator and fires it, so rapid calls from a gesture stay crisp.

Android. Each call runs the activity window’s performHapticFeedback with a HapticFeedbackConstants value. It ignores a view that turned haptics off but still follows the system’s haptics setting. With no activity, such as in the background, the call does nothing. Newer constants are used where the device has them:

CallAndroid 11 and laterEarlier
selectionSegment tick on 14, clock tick beforeClock tick
impact light, softKeyboard tapKeyboard tap
impact medium, rigidVirtual keyVirtual key
impact heavyLong pressLong press
successConfirmVirtual key
warningLong pressLong press
errorRejectContext click

API reference

MethodsTypeDescription
selection() => voidMoving between choices: a picker, a segment, a toggle.
() => voidMoving between choices: a picker, a segment, a toggle.
impact(style: HapticImpact) => voidSomething landed or snapped, at a weight.
(style: HapticImpact) => voidSomething landed or snapped, at a weight.
notification(type: HapticNotification) => voidAn outcome: success, warning, or error.
(type: HapticNotification) => voidAn outcome: success, warning, or error.
performAndroidHaptics(type: AndroidHaptic) => voidAndroid feedback constants directly; no-op on iOS and web.
(type: AndroidHaptic) => voidAndroid feedback constants directly; no-op on iOS and web.

HapticImpact is 'light', 'medium', 'heavy', 'soft', or 'rigid'. HapticNotification is 'success', 'warning', or 'error'. AndroidHaptic is Expo’s AndroidHaptics set as lowercase strings: 'clock-tick', 'confirm', 'context-click', 'drag-start', 'gesture-end', 'gesture-start', 'keyboard-press', 'keyboard-release', 'keyboard-tap', 'long-press', 'no-haptics', 'reject', 'segment-frequent-tick', 'segment-tick', 'text-handle-move', 'toggle-off', 'toggle-on', 'virtual-key', and 'virtual-key-release'. Each name is the Android HapticFeedbackConstants value it plays. A constant the device’s Android version does not have yet plays nothing. From Expo, rename performAndroidHapticsAsync(AndroidHaptics.Confirm) to performAndroidHaptics('confirm'); like Expo, it does nothing off Android, so shared code needs no platform check.

import { One } from 'one'
One.Haptics.performAndroidHaptics('toggle-on')
One.Haptics.performAndroidHaptics('segment-tick')

These calls return void immediately, including on iOS and web.

Platform differences

iOSAndroidWeb
EngineFeedback generatorsView haptic feedbackNone
Soft and rigidTheir own feelSame as light and medium
Follows system settingYesYes

Limits and gotchas

Examples

Tick through a stepper

import { useState } from 'react'
import { Button, Text, View } from 'react-native'
import { One } from 'one'
function Quantity() {
const [count, setCount] = useState(1)
const change = (next: number) => {
if (next < 1 || next > 9) return
One.Haptics.selection()
setCount(next)
}
return (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
<Button title="−" onPress={() => change(count - 1)} />
<Text>{count}</Text>
<Button title="+" onPress={() => change(count + 1)} />
</View>
)
}

The tick plays only when the value changes, so pressing past the limit gives no false feedback.

Confirm a save

import { One } from 'one'
async function saveWithFeedback(save: () => Promise<void>, showError: (message: string) => void) {
try {
await save()
One.Haptics.notification('success')
} catch (error) {
One.Haptics.notification('error')
showError(String(error))
}
}

The success and error patterns feel different, and the error still shows a message for people who have haptics off.

Edit this page on GitHub.