Web Haptics

ShipKit integrates the web-haptics library to provide native-feeling vibration feedback on interactive UI components. Works on Android (via navigator.vibrate()) and iOS Safari (via the <input type="checkbox" switch> trick — no private APIs, no permissions).

How It Works

A HapticsProvider mounts at the root of the app (inside KitProvider) and registers a singleton haptic trigger. Components fire haptic patterns via either:

  1. data-haptic attribute — event delegation on pointerdown (preferred; no client component needed)
  2. Imperative haptic() function — called directly in event handlers

Both approaches are SSR-safe and no-op silently on unsupported devices.

Quick Start

Haptics are enabled by default on all ShipKit interactive components. No setup required.

To disable globally, set the environment variable:

NEXT_PUBLIC_FEATURE_HAPTICS_ENABLED=false

Components with Haptics

ComponentPatternTriggerMethod
Buttonlightclickdata-haptic
Button (destructive)heavyclickdata-haptic
Switchmediumtogglehaptic()
Checkboxselectioncheck/uncheckhaptic()
Togglemediumpresshaptic()
Tabsselectiontab switchhaptic()
Accordionselectionexpand/collapsehaptic()
Dialogsoft open / light closeopen/closehaptic()
Sheetsoft open / light closeopen/closehaptic()
Drawersoft open / light closeopen/closehaptic()
AlertDialog Actionheavyconfirmhaptic()
AlertDialog Cancellightcancelhaptic()
Selectselectionvalue changehaptic()
Sliderlightvalue commithaptic()
Dropdown Menu Itemselectionselect itemhaptic()
Copy Buttonsuccesscopy actionhaptic()
Share (copy link)successcopy actionhaptic()
Toastsuccess / error (destructive)showhaptic()

Available Patterns

All patterns map to web-haptics presets, which match iOS UIFeedbackGenerator categories:

PatternFeelUse Case
lightLight tapGeneral button presses
mediumMedium pulseSwitches, toggles
heavyHeavy thudDestructive/confirm actions
successDouble-pulseCopy, save, success states
warningWarning burstCaution feedback
errorError buzzError states
selectionSelection tickTabs, radio, checkbox, menus
softCushioned tapOpening overlays
rigidCrisp tapHard UI feedback
nudgeReminder nudgeSubtle attention
buzzLong buzzExtended feedback

Usage

Using the data-haptic Attribute (Recommended)

Any element with a data-haptic attribute will automatically fire the specified pattern on pointerdown. No imports needed, works with server components.

<button data-haptic="light">Tap me</button>
<div data-haptic="selection">Select me</div>

The Button component does this by default:

import { Button } from "@/components/ui/button";

// Fires "light" haptic by default
<Button>Save</Button>

// Custom pattern
<Button hapticPattern="success">Submit</Button>

// Disable haptics for this button
<Button noHaptics>No vibration</Button>

Using the haptic() Function (Imperative)

For haptics on events other than pointerdown, or from within event handlers:

import { haptic } from "@/hooks/use-haptics";

function MyComponent() {
  const handleSuccess = () => {
    haptic("success");
    // ... do stuff
  };

  return <div onClick={handleSuccess}>Done</div>;
}

Using the useHaptics() Hook

For full access to all patterns and the isSupported flag:

"use client";

import { useHaptics } from "@/hooks/use-haptics";

function MyComponent() {
  const { tap, toggle, success, error, isSupported, trigger, cancel } = useHaptics();

  return (
    <div>
      <button onClick={tap}>Tap</button>
      <button onClick={success}>Success!</button>
      <button onClick={cancel}>Stop vibrating</button>
      {!isSupported && <p>Haptics not available on this device</p>}
    </div>
  );
}

Architecture

KitProvider
  └─ HapticsProvider              ← mounts singleton + event delegation
       └─ useHaptics()            ← registers web-haptics trigger
            └─ singletonTrigger   ← used by haptic() everywhere

Components use either:
  data-haptic="pattern"           ← caught by HapticsProvider pointerdown listener
  haptic("pattern")               ← imperative, calls singleton directly

Key Files

FilePurpose
src/hooks/use-haptics.tsHook, imperative haptic() function, types
src/components/providers/haptics-provider.tsxSingleton mount + data-haptic event delegation
src/components/providers/kit-provider.tsxWraps app with HapticsProvider

Dependencies

  • web-haptics — core library
  • web-haptics/react — React hook binding (useWebHaptics)

Browser Support

PlatformMethodStatus
Android Chromenavigator.vibrate()✅ Full support
iOS Safari 17.5+<input type="checkbox" switch> trick✅ Works (no permissions)
Desktop browsersAudio debug mode🔊 Audible feedback only
Unsupported—Silent no-op

Button API

The Button component exposes two haptics-related props:

PropTypeDefaultDescription
noHapticsbooleanfalseDisable haptic feedback on this button
hapticPatternHapticPattern"light"Which haptic pattern to fire on click

Adding Haptics to New Components

  1. Option A: data-haptic attribute — add data-haptic="pattern" to the interactive element. Done.
  2. Option B: imperative — import haptic from @/hooks/use-haptics and call it in the event handler:
import { haptic } from "@/hooks/use-haptics";

// In your event handler:
haptic("selection");

No need to wrap in useEffect or call useHaptics() — the singleton is available globally once HapticsProvider mounts.