diff --git a/.changeset/bright-moles-explode.md b/.changeset/bright-moles-explode.md
new file mode 100644
index 0000000..e362754
--- /dev/null
+++ b/.changeset/bright-moles-explode.md
@@ -0,0 +1,5 @@
+---
+"web-haptics": patch
+---
+
+Fix `WebHaptics.isSupported` so it treats touch fallback devices like iPhone as supported, and add `WebHaptics.supportsVibrationApi` for callers that need the narrower `navigator.vibrate` signal.
diff --git a/packages/web-haptics/README.md b/packages/web-haptics/README.md
index 73c292a..0f7e49b 100644
--- a/packages/web-haptics/README.md
+++ b/packages/web-haptics/README.md
@@ -112,7 +112,13 @@ Show or hide the haptic feedback toggle switch.
### `WebHaptics.isSupported`
-Static boolean — `true` if the device supports the Vibration API.
+Static boolean — `true` if WebHaptics can attempt haptics on this device,
+either through the Vibration API or the touch fallback used on platforms like
+iPhone.
+
+### `WebHaptics.supportsVibrationApi`
+
+Static boolean — `true` if the device exposes `navigator.vibrate`.
## License
diff --git a/packages/web-haptics/src/lib/web-haptics/index.ts b/packages/web-haptics/src/lib/web-haptics/index.ts
index e54ca9e..e3eccf2 100644
--- a/packages/web-haptics/src/lib/web-haptics/index.ts
+++ b/packages/web-haptics/src/lib/web-haptics/index.ts
@@ -133,6 +133,39 @@ function toVibratePattern(
let instanceCounter = 0;
+function supportsVibrationApi(): boolean {
+ return (
+ typeof navigator !== "undefined" && typeof navigator.vibrate === "function"
+ );
+}
+
+function supportsTouchFallback(): boolean {
+ if (typeof navigator === "undefined") {
+ return false;
+ }
+
+ if (navigator.maxTouchPoints > 0) {
+ return true;
+ }
+
+ if (typeof window === "undefined") {
+ return false;
+ }
+
+ if ("ontouchstart" in window) {
+ return true;
+ }
+
+ if (typeof window.matchMedia !== "function") {
+ return false;
+ }
+
+ return (
+ window.matchMedia("(pointer: coarse)").matches ||
+ window.matchMedia("(any-pointer: coarse)").matches
+ );
+}
+
export class WebHaptics {
private hapticLabel: HTMLLabelElement | null = null;
private domInitialized = false;
@@ -152,8 +185,13 @@ export class WebHaptics {
this.showSwitch = options?.showSwitch ?? false;
}
- static readonly isSupported: boolean =
- typeof navigator !== "undefined" && typeof navigator.vibrate === "function";
+ static get supportsVibrationApi(): boolean {
+ return supportsVibrationApi();
+ }
+
+ static get isSupported(): boolean {
+ return supportsVibrationApi() || supportsTouchFallback();
+ }
async trigger(
input: HapticInput = [{ duration: 25, intensity: 0.7 }],
diff --git a/packages/web-haptics/src/react/useWebHaptics.ts b/packages/web-haptics/src/react/useWebHaptics.ts
index ac0ce42..2fa152d 100644
--- a/packages/web-haptics/src/react/useWebHaptics.ts
+++ b/packages/web-haptics/src/react/useWebHaptics.ts
@@ -36,6 +36,7 @@ export function useWebHaptics(options?: WebHapticsOptions) {
const cancel = useCallback(() => instanceRef.current?.cancel(), []);
const isSupported = WebHaptics.isSupported;
+ const supportsVibrationApi = WebHaptics.supportsVibrationApi;
- return { trigger, cancel, isSupported };
+ return { trigger, cancel, isSupported, supportsVibrationApi };
}
diff --git a/packages/web-haptics/src/svelte/createWebHaptics.ts b/packages/web-haptics/src/svelte/createWebHaptics.ts
index db51ac9..f67183a 100644
--- a/packages/web-haptics/src/svelte/createWebHaptics.ts
+++ b/packages/web-haptics/src/svelte/createWebHaptics.ts
@@ -14,6 +14,14 @@ export function createWebHaptics(options?: WebHapticsOptions) {
const destroy = () => instance.destroy();
const setDebug = (debug: boolean) => instance.setDebug(debug);
const isSupported = WebHaptics.isSupported;
+ const supportsVibrationApi = WebHaptics.supportsVibrationApi;
- return { trigger, cancel, destroy, setDebug, isSupported };
+ return {
+ trigger,
+ cancel,
+ destroy,
+ setDebug,
+ isSupported,
+ supportsVibrationApi,
+ };
}
diff --git a/packages/web-haptics/src/vue/useWebHaptics.ts b/packages/web-haptics/src/vue/useWebHaptics.ts
index 92c9ab7..7c690db 100644
--- a/packages/web-haptics/src/vue/useWebHaptics.ts
+++ b/packages/web-haptics/src/vue/useWebHaptics.ts
@@ -29,6 +29,7 @@ export function useWebHaptics(options?: WebHapticsOptions) {
instance?.trigger(input, options);
const cancel = () => instance?.cancel();
const isSupported = WebHaptics.isSupported;
+ const supportsVibrationApi = WebHaptics.supportsVibrationApi;
- return { trigger, cancel, isSupported };
+ return { trigger, cancel, isSupported, supportsVibrationApi };
}
diff --git a/site/src/surfaces/docs/index.tsx b/site/src/surfaces/docs/index.tsx
index 59f0731..138c946 100644
--- a/site/src/surfaces/docs/index.tsx
+++ b/site/src/surfaces/docs/index.tsx
@@ -61,7 +61,12 @@ const methods = [
{
signature: "WebHaptics.isSupported: boolean",
description:
- "Static property. Returns true if the device supports the Vibration API.",
+ "Static property. Returns true if WebHaptics can attempt haptics through either the Vibration API or the touch fallback.",
+ },
+ {
+ signature: "WebHaptics.supportsVibrationApi: boolean",
+ description:
+ "Static property. Returns true when the device exposes navigator.vibrate.",
},
];
@@ -175,7 +180,7 @@ export const Docs = () => {
@@ -186,7 +191,7 @@ const { trigger, cancel, isSupported } = useWebHaptics({
@@ -195,7 +200,7 @@ const { trigger, cancel, isSupported } = useWebHaptics();`}