Back to Colophon
Back to Colophon

Discovered web-haptics, a zero-dep wrapper around the Vibration API (with a clever iOS fallback). A handful of prompts later, every interactive element on the site has tactile feedback on mobile.

#mobile#ux#web-components#vibration-api

The library#

web-haptics is a tiny wrapper around the Vibration API with preset patterns for common interactions. No dependencies. It ships with named presets (selection, light, medium, success, error) that map to different vibration durations and sequences.

On Android, it calls navigator.vibrate() directly. On iOS, Safari doesn’t support the Vibration API, so the library does something clever: it creates a hidden <input type="checkbox" switch> and programmatically clicks it. iOS provides Taptic Engine feedback on native switch toggles as a system behavior, so each click produces a physical tap.

How it’s wired#

One shared singleton module (src/lib/haptics.ts) initializes a WebHaptics instance on first call. Every component imports the same haptic() function and passes a preset name. The module checks prefers-reduced-motion and a localStorage toggle before triggering anything.

Which interactions got which preset:

  • selection — theme toggle, haptics toggle, command palette result selection, copy buttons
  • light — mobile menu open/close, command palette open, dropdown open/close, link preview hover
  • success — form submissions, copy-to-clipboard confirmation
  • error — form validation failures
  • medium — schedule modal open

The toggle#

Not everyone wants their phone buzzing while they read. A HapticsToggle web component sits in the nav next to the theme toggle. It reads and writes localStorage.getItem('haptics'), with 'on' as the default. The haptic() function checks this value on every call.

The toggle fires one last vibration before disabling (so you feel the “off” click) and one vibration on re-enable (so you know it worked). Same pattern as the theme toggle: web component with connectedCallback/disconnectedCallback for View Transition survival, data-haptics attribute on <html> for CSS state, inline script in <head> to prevent flash of wrong state.

Reduced motion#

The haptic() function bails early if prefers-reduced-motion: reduce is set at the OS level. This runs before the localStorage check, so system preferences always win. Users who’ve told their OS they want less motion don’t get vibrations regardless of the toggle state.

Low Power Mode#

Android’s Battery Saver suppresses navigator.vibrate() at the OS level, so haptics stop automatically. iOS is a different story. The hidden switch checkbox trick bypasses Low Power Mode because iOS treats it as normal UI feedback, not a vibration request.

There’s no web API to detect Low Power Mode on iOS. navigator.getBattery() doesn’t exist in Safari, and the timer throttling heuristics that worked on older iOS versions (WebKit clamping setInterval and requestAnimationFrame to 30fps in LPM) no longer produce a detectable signal on iOS 18. The timers run at full 60fps regardless of power mode. So the nav toggle is the only way for iOS users to disable haptics manually.

The haptics gallery demos all 11 presets the library ships, including 6 we haven’t wired up yet.

Discovered web-haptics, a zero-dep wrapper around the Vibration API (with a clever iOS fallback). A handful of prompts later, every interactive element on the site has tactile feedback on mobile.