i tried to build haptic components for the web. then i found the webkit commit that kills it.

march 2, 202610 min read

Web apps feel flat. You tap a button, drag a slider, spin a picker — nothing pushes back. Native apps have had haptics for years. That satisfying click when you toggle a switch in iOS Settings? Android buzzing on every interaction? The web has none of that.

So I tried to fix it. I wanted to build haptic UI components for the web — rotary knobs with detent feedback, scroll pickers with ticks, swipe cards with tension. The kind of interactions that make you forget you're using a browser.

It worked beautifully on Android. On iOS, I hit a wall that led me into the WebKit source code, and what I found there was pretty discouraging.

The State of Web Haptics

There are basically two ways to do haptics on the web:

The Vibration API (navigator.vibrate()) — simple, flexible, works great on Android. You pass in durations in milliseconds:

navigator.vibrate([50])                  // light tap
navigator.vibrate([100])                 // medium impact
navigator.vibrate([40, 80, 40, 80, 60]) // success pattern

iOS Safari doesn't support it. Never has. There's an open interop issue with 30+ upvotes. Apple hasn't said a word.

The Safari Switch Trick — the only known workaround for iOS. Safari 17.4 added <input type="checkbox" switch>, a native toggle element. On iOS 18+, toggling it fires the Taptic Engine. Same haptic you feel in native apps.

The trick is to create a hidden switch and programmatically click its label:

const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.setAttribute('switch', '')
checkbox.id = '_haptic'

const label = document.createElement('label')
label.htmlFor = '_haptic'

document.body.appendChild(checkbox)
document.body.appendChild(label)

label.click() // Taptic Engine fires!

This actually works. Libraries like ios-haptics are built around it. So I went ahead and built 10 components using both approaches — Vibration API for Android, switch trick for iOS.

What I Built

I built 10 components in a single demo page — rotary knobs, combination locks, textured sliders, pull-to-confirm, a haptic counter, sortable lists, segmented controls, a color picker, a scroll wheel picker, and swipe cards. All of them wired up with haptic feedback.

Here's the rotary knob. Try spinning it on your phone:

Rotary knob with 24 detent positions. On Android, you'll feel each click.

On Android, every component felt incredible. Spinning the knob gave you this satisfying click-click-click on each notch. The slider buzzed at every step. The scroll picker felt like a native iOS date picker. For the first time, a web app felt physical.

On iOS? Only two components worked — the Counter and the Segmented Control. Both tap-based. Everything that involved dragging? Dead silent.

The counter works on both iOS and Android. Tap-based haptics pass WebKit's user gesture check.

The Debugging Rabbit Hole

First thought: throttling. Maybe Safari rate-limits the switch trick and rapid calls during a drag get dropped. I built a throttle test — fire 5 haptics at intervals from 50ms to 500ms. All worked fine from button taps.

Next thought: element visibility. Maybe Safari blocks haptics for off-screen elements. Tested every hiding strategy — position: fixed; top: -99px, overflow: hidden, opacity: 0, 1x1 pixel. All worked from clicks.

The pattern became obvious: any haptic triggered from a click/tap works. Any haptic triggered from a pointermove doesn't. Not throttling. Not visibility. The event type itself.

I threw everything at it — setTimeout(fn, 0), requestAnimationFrame, MessageChannel, queueMicrotask, creating and destroying fresh switch elements on every fire. Nothing worked inside a drag gesture.

The WebKit Commit

So I went looking through the WebKit source. And found commit dfb3971, from January 3, 2025:

"Haptic feedback for <input type=checkbox switch> should require user activation"

It should not be possible to generate haptic feedback from script alone. Check that a user gesture is being processed if the haptic feedback is triggered due to a click.

And then the line that explained everything:

SwitchTrigger::PointerTracking is not checked since it is only generated by trusted mouse move events, and processingUserGesture is not true for mouse moves.

That's it. WebKit checks processingUserGesture before allowing haptic feedback. Clicks and taps count as user gestures. Pointer moves don't. This is enforced in C++ in CheckboxInputType.cpp — no JavaScript trick can get around it.

The commit landed in January 2025, after iOS 18.0 shipped. The switch trick probably worked for continuous gestures in the initial release. Apple patched it out on purpose.

What This Means

Every web haptics library using the switch trick has this limitation. As far as I can tell, none of them document it. ios-haptics (12K+ weekly downloads), react-haptic, every blog post about the technique — they all quietly omit that it only works for discrete taps.

Works on iOS Safari: Button clicks, toggle switches, tab changes, steppers, form submissions — anything that's a single tap.

Doesn't work (and won't, unless Apple decides otherwise): Drag feedback, scroll picker ticks, knob detents, slider notches, swipe tension — anything continuous.

Android? Everything works. Has for over 12 years.

The Bigger Picture

Apple's been slow to adopt web APIs that make web apps competitive with native ones. The Vibration API has been in Chrome and Firefox since 2013. Safari still hasn't implemented it.

The switch trick was a side effect — Apple added a native UI element, not a haptics API. When developers started using it for programmatic haptics, Apple locked it down. Same pattern as autoplay, notifications, and other APIs they consider abuse vectors.

The W3C interop issue requesting Vibration API support has been open since 2024. Apple hasn't commented. Until they implement it or provide some other web haptics API, continuous haptic feedback on iOS is just not possible.

Is a Haptic Library Still Worth Building?

Yeah, I think so — just with honest expectations. A library that gives you semantic patterns (haptic.success(), haptic.tick()), automatic platform detection, full support on Android (72% of global mobile users), and tap-level haptics on iOS — that's still genuinely useful. It's better than the zero haptics most web apps have today.

It's just not the "make web apps feel native everywhere" thing I was going for. The web isn't there yet on iOS, and only Apple can change that.

Try It Yourself

Open the full demo on your phone. On Android, every component has full haptic feedback. On iOS, tap the Counter and Segmented Control to feel what works — then try the Knob and feel the silence.


If you're interested in the WebKit source, the relevant file is Source/WebCore/html/CheckboxInputType.cpp and the bug tracker entry is webkit.org/b/285120.

← back to blog