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).
A HapticsProvider mounts at the root of the app (inside KitProvider) and registers a singleton haptic trigger. Components fire haptic patterns via either:
data-haptic attribute — event delegation on pointerdown (preferred; no client component needed)haptic() function — called directly in event handlersBoth approaches are SSR-safe and no-op silently on unsupported devices.
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
| Component | Pattern | Trigger | Method |
|---|---|---|---|
| Button | light | click | data-haptic |
| Button (destructive) | heavy | click | data-haptic |
| Switch | medium | toggle | haptic() |
| Checkbox | selection | check/uncheck | haptic() |
| Toggle | medium | press | haptic() |
| Tabs | selection | tab switch | haptic() |
| Accordion | selection | expand/collapse | haptic() |
| Dialog | soft open / light close | open/close | haptic() |
| Sheet | soft open / light close | open/close | haptic() |
| Drawer | soft open / light close | open/close | haptic() |
| AlertDialog Action | heavy | confirm | haptic() |
| AlertDialog Cancel | light | cancel | haptic() |
| Select | selection | value change | haptic() |
| Slider | light | value commit | haptic() |
| Dropdown Menu Item | selection | select item | haptic() |
| Copy Button | success | copy action | haptic() |
| Share (copy link) | success | copy action | haptic() |
| Toast | success / error (destructive) | show | haptic() |
All patterns map to web-haptics presets, which match iOS UIFeedbackGenerator categories:
| Pattern | Feel | Use Case |
|---|---|---|
light | Light tap | General button presses |
medium | Medium pulse | Switches, toggles |
heavy | Heavy thud | Destructive/confirm actions |
success | Double-pulse | Copy, save, success states |
warning | Warning burst | Caution feedback |
error | Error buzz | Error states |
selection | Selection tick | Tabs, radio, checkbox, menus |
soft | Cushioned tap | Opening overlays |
rigid | Crisp tap | Hard UI feedback |
nudge | Reminder nudge | Subtle attention |
buzz | Long buzz | Extended feedback |
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>
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>;
}
useHaptics() HookFor 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>
);
}
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
| File | Purpose |
|---|---|
src/hooks/use-haptics.ts | Hook, imperative haptic() function, types |
src/components/providers/haptics-provider.tsx | Singleton mount + data-haptic event delegation |
src/components/providers/kit-provider.tsx | Wraps app with HapticsProvider |
web-haptics — core libraryweb-haptics/react — React hook binding (useWebHaptics)| Platform | Method | Status |
|---|---|---|
| Android Chrome | navigator.vibrate() | ✅ Full support |
| iOS Safari 17.5+ | <input type="checkbox" switch> trick | ✅ Works (no permissions) |
| Desktop browsers | Audio debug mode | 🔊 Audible feedback only |
| Unsupported | — | Silent no-op |
The Button component exposes two haptics-related props:
| Prop | Type | Default | Description |
|---|---|---|---|
noHaptics | boolean | false | Disable haptic feedback on this button |
hapticPattern | HapticPattern | "light" | Which haptic pattern to fire on click |
data-haptic attribute — add data-haptic="pattern" to the interactive element. Done.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.