- Accordion
- Action Sheet
- Alert Dialog
- Alert
- Audio Player
- Audio Recorder
- Audio Waveform
- Avatar
- AvoidKeyboard
- Badge
- BottomSheet
- Button
- Camera Preview
- Camera
- Card
- Carousel
- Checkbox
- Collapsible
- Color Picker
- Combobox
- Date Picker
- File Picker
- Gallery
- Hello Wave
- Icon
- Image
- Input OTP
- Input
- Link
- MediaPicker
- Mode Toggle
- Onboarding
- ParallaxScrollView
- Picker
- Popover
- Progress
- Radio
- ScrollView
- SearchBar
- Separator
- Share
- Sheet
- Skeleton
- Spinner
- Switch
- Table
- Tabs
- Text
- Toast
- Toggle
- Video
- View
Installation
pnpm dlx bna-ui add useHaptics
Usage
import { useHaptics, triggerHaptic } from '@/hooks/useHaptics';export function SaveButton({ haptic = true, onSave }) {
const feedback = useHaptics(haptic);
return (
<Button
haptic={false}
onPress={() => {
feedback('success');
onSave();
}}
>
Save
</Button>
);
}You describe what the haptic is for — a selection, a toggle, a success — and
the hook picks the API. Every component in this library that has a haptic
prop is built on it, so you rarely call it directly unless you are building
your own control.
import { useHaptics } from '@/hooks/useHaptics';
export function Stepper({ value, onChange, haptic = true }) {
const feedback = useHaptics(haptic);
return (
<Button
haptic={false}
onPress={() => {
feedback('tick');
onChange(value + 1);
}}
>
+
</Button>
);
}API Reference
useHaptics
const feedback = useHaptics(enabled?: boolean);Returns a stable trigger (intent?: HapticIntent) => void that does nothing
while enabled is false. Its identity only changes when enabled does, so
it is safe in a useCallback dependency array and will not defeat a
React.memo boundary.
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether the returned trigger fires. Pass a haptic prop here. |
triggerHaptic
triggerHaptic(intent?: HapticIntent): void;The module-level equivalent, for call sites that are not component bodies — a
render prop, a handler defined outside a component, a runOnJS target.
Both are fire-and-forget: they never throw and never reject. A device with no taptic engine, or a build where the native module is missing, must not be able to take a press handler down with it.
Platform Differences
iOS and web share one API surface. Web is not a no-op — Expo drives the Web Vibration API there, falling back to an iOS-Safari switch-element trick.
Android is the reason this hook exists. impactAsync and notificationAsync
are simulated on Android with the raw
Vibrator
service, which Expo explicitly does not recommend: it is a coarse timed buzz,
nothing like a native Android control.
performAndroidHapticsAsync
goes through View.performHapticFeedback instead — the same engine those
controls use.
| Intent | iOS & Web | Android |
|---|---|---|
selection | selectionAsync() | Segment_Tick |
tick | selectionAsync() | Clock_Tick |
toggle-on | impactAsync(Light) | Toggle_On |
toggle-off | impactAsync(Light) | Toggle_Off |
impact-light | impactAsync(Light) | Virtual_Key |
impact-medium | impactAsync(Medium) | Long_Press |
success | notificationAsync(Success) | Confirm |
warning | notificationAsync(Warning) | Reject |
error | notificationAsync(Error) | Reject |
tick maps to Clock_Tick rather than Segment_Frequent_Tick deliberately:
the latter is documented as possibly producing no vibration at all on devices
that cannot make a suitably soft one, which would silently drop repeated ticks
on cheaper actuators.
When feedback is silent
Haptics can be unavailable for reasons that are not bugs:
- Android — the user turned off Settings → Sound & vibration →
Vibration & haptics → Touch feedback.
performHapticFeedbackis then suppressed system-wide. - iOS — Low Power Mode is on, the user disabled the Taptic Engine, or the camera or dictation is active (iOS silences haptics to protect both).
- Web — the browser does not support the Vibration API, the device has no vibration hardware, or the tab is backgrounded.
- Any platform — the component was given
haptic={false}.