useHaptics

PreviousNext

Semantic haptic feedback that routes each intent to the right native API per platform, using performAndroidHapticsAsync on Android instead of the Vibrator-simulated impact APIs.

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.

ParameterTypeDefaultDescription
enabledbooleantrueWhether 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.

IntentiOS & WebAndroid
selectionselectionAsync()Segment_Tick
tickselectionAsync()Clock_Tick
toggle-onimpactAsync(Light)Toggle_On
toggle-offimpactAsync(Light)Toggle_Off
impact-lightimpactAsync(Light)Virtual_Key
impact-mediumimpactAsync(Medium)Long_Press
successnotificationAsync(Success)Confirm
warningnotificationAsync(Warning)Reject
errornotificationAsync(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. performHapticFeedback is 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}.