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.
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.
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:
| Call | Android 11 and later | Earlier |
|---|---|---|
| selection | Segment tick on 14, clock tick before | Clock tick |
| impact light, soft | Keyboard tap | Keyboard tap |
| impact medium, rigid | Virtual key | Virtual key |
| impact heavy | Long press | Long press |
| success | Confirm | Virtual key |
| warning | Long press | Long press |
| error | Reject | Context click |
| Methods | Type | Description |
|---|---|---|
selection() => voidMoving between choices: a picker, a segment, a toggle. | () => void | Moving between choices: a picker, a segment, a toggle. |
impact(style: HapticImpact) => voidSomething landed or snapped, at a weight. | (style: HapticImpact) => void | Something landed or snapped, at a weight. |
notification(type: HapticNotification) => voidAn outcome: success, warning, or error. | (type: HapticNotification) => void | An outcome: success, warning, or error. |
performAndroidHaptics(type: AndroidHaptic) => voidAndroid feedback constants directly; no-op on iOS and web. | (type: AndroidHaptic) => void | Android 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.
| iOS | Android | Web | |
|---|---|---|---|
| Engine | Feedback generators | View haptic feedback | None |
| Soft and rigid | Their own feel | Same as light and medium | |
| Follows system setting | Yes | Yes |
selection while a value changes,
impact when something lands, and notification once per outcome. A
success pattern on every tap stops meaning anything.selection when a value crosses to
a new step, not on every move event.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.
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.