diff --git a/web/src/components/buttons/SettingsButton.svelte b/web/src/components/buttons/SettingsButton.svelte
index 871c2fa0..0105d8e8 100644
--- a/web/src/components/buttons/SettingsButton.svelte
+++ b/web/src/components/buttons/SettingsButton.svelte
@@ -6,6 +6,8 @@
Value extends CobaltSettings[Context][Id]
"
>
+ import { hapticSwitch } from "$lib/haptics";
+
import settings, { updateSetting } from "$lib/state/settings";
import type { CobaltSettings } from "$lib/types/settings";
@@ -22,12 +24,14 @@
class="button"
class:active={isActive}
aria-pressed={isActive}
- on:click={() =>
+ on:click={() => {
+ hapticSwitch();
updateSetting({
[settingContext]: {
[settingId]: settingValue,
},
- })}
+ });
+ }}
>
diff --git a/web/src/components/buttons/SettingsToggle.svelte b/web/src/components/buttons/SettingsToggle.svelte
index f5dd6579..14edbb66 100644
--- a/web/src/components/buttons/SettingsToggle.svelte
+++ b/web/src/components/buttons/SettingsToggle.svelte
@@ -5,6 +5,7 @@
Id extends keyof CobaltSettings[Context]
"
>
+ import { hapticSwitch } from "$lib/haptics";
import settings, { updateSetting } from "$lib/state/settings";
import type { CobaltSettings } from "$lib/types/settings";
@@ -34,14 +35,15 @@
class="button toggle-container"
role="switch"
aria-checked={isEnabled}
- disabled={disabled}
- on:click={() =>
+ {disabled}
+ on:click={() => {
+ hapticSwitch();
updateSetting({
[settingContext]: {
[settingId]: !isEnabled,
- }
- })
- }
+ },
+ });
+ }}
>
{title}
@@ -82,10 +84,14 @@
border-radius: var(--border-radius);
overflow: scroll;
- transition: background 0.1s, box-shadow 0.1s;
+ transition:
+ background 0.1s,
+ box-shadow 0.1s;
}
.toggle-container:active {
- box-shadow: var(--button-box-shadow), 0 0 0 1.5px var(--button-stroke) inset;
+ box-shadow:
+ var(--button-box-shadow),
+ 0 0 0 1.5px var(--button-stroke) inset;
}
diff --git a/web/src/lib/device.ts b/web/src/lib/device.ts
index e0864963..42f19aa5 100644
--- a/web/src/lib/device.ts
+++ b/web/src/lib/device.ts
@@ -11,6 +11,7 @@ const device = {
iPhone: false,
iPad: false,
iOS: false,
+ modernIOS: false,
android: false,
mobile: false,
},
@@ -35,6 +36,9 @@ if (browser) {
const iPhone = ua.includes("iphone os");
const iPad = !iPhone && ua.includes("mac os") && navigator.maxTouchPoints > 0;
+ const iosVersion = Number(ua.match(/iphone os (\d+)_/)?.[1]);
+ const modernIOS = iPhone && iosVersion >= 18;
+
const iOS = iPhone || iPad;
const android = ua.includes("android") || ua.includes("diordna");
@@ -45,11 +49,13 @@ if (browser) {
};
device.is = {
+ mobile: iOS || android,
+ android,
+
iPhone,
iPad,
iOS,
- android,
- mobile: iOS || android,
+ modernIOS,
};
device.browser = {
diff --git a/web/src/lib/haptics.ts b/web/src/lib/haptics.ts
new file mode 100644
index 00000000..22937832
--- /dev/null
+++ b/web/src/lib/haptics.ts
@@ -0,0 +1,26 @@
+import { device } from "$lib/device";
+
+// not sure if vibrations feel the same on android,
+// so they're enabled only on ios 18+ for now
+const shouldVibrate = device.is.modernIOS;
+
+export const hapticSwitch = () => {
+ if (!shouldVibrate) return;
+
+ try {
+ const label = document.createElement("label");
+ label.ariaHidden = "true";
+ label.style.display = "none";
+
+ const input = document.createElement("input");
+ input.type = "checkbox";
+ input.setAttribute("switch", "");
+ label.appendChild(input);
+
+ document.head.appendChild(label);
+ label.click();
+ document.head.removeChild(label);
+ } catch {
+ // ignore
+ }
+}