Web Vibration API: Haptic Feedback on Android and iPhone
You want a tap to tick under the thumb the way it does in a native app, so you reach for the web vibration API. On an Android phone navigator.vibrate(10) works on the first try. On an iPhone nothing happens, on a laptop the call returns true and nothing happens, and the iPhone workaround you found on GitHub stopped working in iOS 26.5. This tutorial maps what a page can really trigger in October 2026, builds a helper that stays silent wherever it cannot play, and wires haptics into a switch, a button and a wheel picker in React.
What can the Web Vibration API do in 2026?
It can switch the vibration motor on and off for durations you give in milliseconds, in Chromium browsers on Android, after the user has tapped the page. That is the whole API: one method, navigator.vibrate, that takes a number or an array of alternating vibrate and pause times. There is no intensity, no waveform and no way to ask whether the device has a motor at all. Software Mansion's August 2026 overview of web haptics (opens in a new tab) sums up the support in one line: "the vibration API is effectively limited to Chromium-based browsers."
Here is where a tick can reach a finger, as of October 2026 (caniuse (opens in a new tab) for support, our own test in desktop Chromium for the last row):
The last row is the trap. Feature detection with "vibrate" in navigator says yes in desktop Chrome, so it tells you nothing about hardware. The boolean that navigator.vibrate() returns only says the browser accepted the pattern; it says nothing about whether anyone felt it.
How does navigator.vibrate behave on Android?
It plays the pattern you pass and replaces any pattern already running, within limits you can read in Chromium's vibration controller (opens in a new tab):
- It needs a tap first. MDN (opens in a new tab) lists sticky user activation as a requirement. Until the page has seen a tap, a click or a key press, Chrome returns
falseand logs "Blocked call to navigator.vibrate because user hasn't tapped on the frame or any embedded frame yet". Once granted, the activation lasts until the page navigates, so a tick from a scroll handler or after anawaitworks later on. - It is silent in the background. The call returns
falsewhile the page is hidden. - Long patterns are trimmed. Chromium caps each entry at 10,000ms, keeps the first 99 entries and drops a trailing pause.
- The phone can overrule you. MDN notes that some devices do not vibrate in silent or Do Not Disturb mode.
Keep the pulses short; the helper below uses 4 to 22ms per pulse. The 200ms buzz in many examples (MDN's first one included) reads as a notification, which is the wrong message for a toggle.
Does the Web Vibration API work on iOS?
No. Safari has never implemented navigator.vibrate, and caniuse lists no version of Safari on iOS with it, up to 27.2. Look up web vibration API iOS support and you land on one workaround: Safari 17.4 added a native switch control, <input type="checkbox" switch> (WebKit's announcement (opens in a new tab)), and toggling it plays the system haptic tick.
Until iOS 26.5, libraries such as ios-haptics played that tick on demand by calling .click() on a hidden label wired to the switch. That no longer works. The ios-haptics README of April 2026 (opens in a new tab) says the trick "only works on ios 17.4 to 26.4, as apple patched it in ios 26.5". The WebKit fix (opens in a new tab), merged into the main branch on May 21, 2026, closes that gap: WebKit already required user activation for the haptic, and now it also requires a trusted event, which a click forwarded from label.click() no longer is.
What still works is a real tap that lands on a label for the switch. The tap produces a trusted click, the label forwards it to the switch, and because the original click was trusted, the WebKit change still lets the switch tick. The ios-haptics source (opens in a new tab) moved to exactly that on September 22, 2026: it lays a transparent label over your element. Here is the same idea as a React 19 component:
Use it in buttons, not links: in our Chromium test a label inside an <a> ran the link's onClick but took over the default action, so the link never navigated. Leave the input without a name, so it never ends up in a form submission. Outside iOS a tap on it only toggles a hidden checkbox nobody sees, so it can render on every platform and never causes a hydration mismatch.
On an iPhone you get one fixed tick per tap, from the tap itself: no patterns, nothing after an await, nothing during a drag, and nothing for a wheel spinning past rows, because a scroll never produces a click.
How do you call navigator.vibrate safely where it is unsupported?
Check for the method itself, check activation, catch everything, and return a boolean. Here is the whole helper without dependencies:
The Node check matters. Node 21 and later define a global navigator with no vibrate, so a helper that only checks typeof navigator gets past its guard on the server and calls a method that is not there.
Wingo UI's haptics helper is the same idea plus an iPhone fallback and a force option that plays a tick even when haptics are off. haptic(kind) returns whether a haptic was sent, configureHaptics({ enabled, iosFallback }) sets the app-wide options, and supportsHaptics() tells you whether the browser exposes a haptic path at all (call it in an effect: it is false on the server, and true in desktop Chrome for the reason above).
$ npx wingo-ui@latest add hapticsA caveat about our own code: the helper's iosFallback option, on by default, still clicks a hidden label from script. That plays on iPhones up to iOS 26.4, is ignored from 26.5, and the call still returns true there because WebKit reports nothing back. If you add the tap overlay above, call configureHaptics({ iosFallback: false }) once so iPhones on older versions do not tick twice.
The snippets below import Wingo UI's helper. With the dependency-free file above, import vibrate from @/lib/vibrate instead and use setHapticsEnabled in place of configureHaptics; it has no force option.
Which haptic should each interaction get?
One tick per event, sized to the event. These are the patterns the helper sends on Android:
Three rules keep it from turning into noise. Call it from the handler of a real gesture, never from an effect or a render. Never tick per animation frame; tick when something changes state, like a row reaching the center. And never put information only in the tick: desktop users, iPhone users on a wheel and anyone with a phone on silent will not feel it, so the screen must say the same thing.
How do you add React haptic feedback to a switch, a button and a wheel?
Call haptic() in the handler where the state changes, once per change.
A switch
The free Switch calls onCheckedChange on every click, key or drag, and a returned promise makes it spin, then flip back with errorText if the save fails. Tick on the change and again on failure:
On iOS 26.5 and later this Switch stays silent: it is a Radix button with role="switch", not the native input, so no system tick comes with the tap, and haptic() cannot reach the switch tick from script. If an iPhone tick on a settings toggle matters more to you than the custom look, a plain <input type="checkbox" switch> ticks on its own and renders as a checkbox in browsers that do not know the attribute.
A button
The Haptic Button plays its hapticKind (default light) on touch and pen presses only, so a mouse click never asks for a vibration. In mode="hold" it plays light when the hold starts, success once onConfirm has finished, warning for a tap that was too short and error when onConfirm rejects; with onLongPress set, a long press plays medium.
$ npx wingo-ui@latest add haptic-buttonFor an ordinary Button that should tick on both platforms, combine the call and the overlay. The Button is already position: relative, so the overlay covers it:
A wheel
The Picker Wheel plays a selection tick each time a new row reaches the center while a finger, a mouse drag or a scroll wheel drives it, and never for keys, clicks or value changes from outside. haptics={false} turns it off.
$ npx wingo-ui@latest add picker-wheelThis is the clearest case of the platform gap. On Android every row that passes the center ticks. On an iPhone the wheel is silent from iOS 26.5, because a spin is a scroll and never produces the trusted click the switch needs; the band, the tilt and the snap carry the feedback there.
How do you let people turn haptics off?
Give them a setting and pass it to the helper once. Every haptic feedback web app needs one: some people find vibration distracting, and the browser gives a page no way to read the phone's own haptics setting.
Store the value with the rest of the user's settings. Every component that ticks through the helper follows it without a prop: the Haptic Button, the Picker Wheel, and the Pull To Refresh from our pull to refresh guide among them.
What comes after navigator.vibrate?
A semantic API with named effects, if browsers adopt the one being drafted now. The Web Haptics API (opens in a new tab) started as a proposal to the CSS Working Group (opens in a new tab) in March 2026 and has been incubated at the WICG since April. Its draft has a navigator.playHaptics(effect, intensity) call and a nested @haptic CSS rule, with six named effects (hint, edge, tick, align, success, error) and an intensity from 0 to 1 that the browser maps to the platform's own haptics. The call returns undefined, so a page learns nothing about the device. As of October 2026 Chrome Platform Status (opens in a new tab) lists Web haptics as Proposed, and no stable browser ships it.
The shape is familiar: named kinds instead of millisecond patterns. If your app already funnels every tick through one haptic(kind) call, moving to a new API later means changing one file.
Where should you start?
Add a helper first, then tick the three places users touch most: toggles, the primary button and any wheel or slider. The dependency-free lib/vibrate.ts above covers Android. npx wingo-ui@latest add haptics copies Wingo UI's lib/haptics.ts instead; it is part of Wingo UI Pro, like the Haptic Button and the Picker Wheel, while the Switch is free. For the rest of the phone checklist (bottom sheets, 44px touch targets, safe area insets), read mobile-first React components or browse the Mobile UI in React category.
FAQ
Does navigator.vibrate work on iPhone?
No. Safari has never implemented the Vibration API, and as of October 2026 caniuse lists no version of Safari on iOS that supports it. The only haptic a web page can produce on an iPhone is the system tick of a native switch input, and since iOS 26.5 only the user's own tap on it plays that tick.
Why does navigator.vibrate return true on my laptop?
Because true only means the browser accepted the pattern. Desktop Chrome exposes navigator.vibrate and, once the page has seen a click, returns true for a valid pattern even though there is no motor to run. Use the return value to detect refusals and never to detect hardware.
Why does navigator.vibrate do nothing on my Android phone?
The usual causes are a page the user has not tapped yet (Chrome blocks the call and logs a warning until the page has sticky user activation), a hidden tab, silent or Do Not Disturb mode, or Firefox, which removed the API in version 129.
Can a web page control vibration intensity?
Not with the Vibration API, which only switches the motor on and off for the durations you pass. The Web Haptics API proposal, incubated at the WICG since April 2026, adds named effects with an intensity from 0 to 1, but as of October 2026 no stable browser ships it.
Is the Wingo UI haptics helper free?
No. The haptics helper, the Haptic Button and the Picker Wheel are Pro items; the Switch is free. With a Pro account signed in through npx wingo-ui@latest login, npx wingo-ui@latest add haptics copies lib/haptics.ts into your project.
- Mobile UI
- Haptics
- Vibration API
- iOS
- React