From 2a549a91356cea6b3262f8ee3fdfd05946625e35 Mon Sep 17 00:00:00 2001
From: Thomas Steiner
Date: Sat, 4 Jul 2026 16:13:26 +0200
Subject: [PATCH 1/4] feat(ui): replace custom toggle with native input switch
polyfill
Uses the new element, polyfilled via
input-switch-polyfill for browsers that don't support it yet, kept
live-synced with the accent color and color-mode changes.
---
app/components/Settings/Toggle.client.vue | 42 ++-------------------
app/plugins/input-switch-polyfill.client.ts | 26 +++++++++++++
nuxt.config.ts | 2 +-
package.json | 1 +
pnpm-lock.yaml | 8 ++++
5 files changed, 39 insertions(+), 40 deletions(-)
create mode 100644 app/plugins/input-switch-polyfill.client.ts
diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue
index 29b07f6b62..9a4864b6fb 100644
--- a/app/components/Settings/Toggle.client.vue
+++ b/app/components/Settings/Toggle.client.vue
@@ -54,51 +54,15 @@ const id = useId()
{{ label }}
{{ description }}
-
-
diff --git a/app/plugins/input-switch-polyfill.client.ts b/app/plugins/input-switch-polyfill.client.ts
new file mode 100644
index 0000000000..46dc136dd4
--- /dev/null
+++ b/app/plugins/input-switch-polyfill.client.ts
@@ -0,0 +1,26 @@
+export default defineNuxtPlugin(async () => {
+ if (!('switch' in HTMLInputElement.prototype)) {
+ // @ts-expect-error input-switch-polyfill ships no types; imported for its side effect only
+ await import('input-switch-polyfill')
+
+ // The polyfill reads `accent-color` once at upgrade time and freezes the
+ // resolved color as `--switch-accent` in each element's inline style.
+ // Re-sync whenever the accent or color-mode changes so switches react live.
+ const syncSwitchAccent = () => {
+ const switches = document.querySelectorAll('input.switch')
+ const [first] = switches
+ if (!first) return
+ // All switches share the same cascade; read from the first one.
+ const color = getComputedStyle(first).getPropertyValue('accent-color').trim()
+ if (!color || color === 'auto') return
+ for (const el of switches) {
+ el.style.setProperty('--switch-accent', color)
+ }
+ }
+
+ new MutationObserver(syncSwitchAccent).observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['style', 'class'],
+ })
+ }
+})
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 401dfd006e..ec29109b1a 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -33,7 +33,7 @@ export default defineNuxtConfig({
storageKey: 'npmx-color-mode',
},
- css: ['~/assets/main.css'],
+ css: ['~/assets/main.css', 'input-switch-polyfill/input-switch-polyfill.css'],
runtimeConfig: {
sessionPassword: '',
diff --git a/package.json b/package.json
index a83f9ec1c9..a765e013d2 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"focus-trap": "^8.0.0",
"gray-matter": "4.0.3",
"hls.js": "1.6.16",
+ "input-switch-polyfill": "1.12.0",
"ipaddr.js": "2.3.0",
"marked": "18.0.0",
"module-replacements": "3.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4ffe4c756c..6db97f9011 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -186,6 +186,9 @@ importers:
hls.js:
specifier: 1.6.16
version: 1.6.16
+ input-switch-polyfill:
+ specifier: 1.12.0
+ version: 1.12.0
ipaddr.js:
specifier: 2.3.0
version: 2.3.0
@@ -8001,6 +8004,9 @@ packages:
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ input-switch-polyfill@1.12.0:
+ resolution: {integrity: sha512-4IF+lwC4jjInFPWWjHDcVJWvWiTHm1RnGa+P9uLhOleal69NPbUnrcPHJJhuexyUqOnyA0QenXhtZNuZX4gqWA==}
+
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -19588,6 +19594,8 @@ snapshots:
ini@4.1.1: {}
+ input-switch-polyfill@1.12.0: {}
+
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
From 21f95803563e1ceb4fbb57d6cb0540af1598a022 Mon Sep 17 00:00:00 2001
From: Thomas Steiner
Date: Fri, 2 Oct 2026 11:11:30 +0200
Subject: [PATCH 2/4] chore: retrigger CI
From 8f7b611976bf11ef937dbd5ac67af77b7bf51dee Mon Sep 17 00:00:00 2001
From: Felix Schneider <99918022+trueberryless@users.noreply.github.com>
Date: Sun, 1 Nov 2026 15:06:37 +0100
Subject: [PATCH 3/4] fix(ui): respect reduced motion and load switch polyfill
in parallel
---
app/components/Settings/Toggle.client.vue | 9 ++++
app/plugins/input-switch-polyfill.client.ts | 48 +++++++++++----------
2 files changed, 35 insertions(+), 22 deletions(-)
diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue
index 9a4864b6fb..4750e77a62 100644
--- a/app/components/Settings/Toggle.client.vue
+++ b/app/components/Settings/Toggle.client.vue
@@ -66,3 +66,12 @@ const id = useId()
{{ description }}
+
+
diff --git a/app/plugins/input-switch-polyfill.client.ts b/app/plugins/input-switch-polyfill.client.ts
index 46dc136dd4..1cb57ccc92 100644
--- a/app/plugins/input-switch-polyfill.client.ts
+++ b/app/plugins/input-switch-polyfill.client.ts
@@ -1,26 +1,30 @@
-export default defineNuxtPlugin(async () => {
- if (!('switch' in HTMLInputElement.prototype)) {
- // @ts-expect-error input-switch-polyfill ships no types; imported for its side effect only
- await import('input-switch-polyfill')
+export default defineNuxtPlugin({
+ // Don't hold up hydration for a polyfill that only the settings page needs.
+ parallel: true,
+ async setup() {
+ if (!('switch' in HTMLInputElement.prototype)) {
+ // @ts-expect-error input-switch-polyfill ships no types; imported for its side effect only
+ await import('input-switch-polyfill')
- // The polyfill reads `accent-color` once at upgrade time and freezes the
- // resolved color as `--switch-accent` in each element's inline style.
- // Re-sync whenever the accent or color-mode changes so switches react live.
- const syncSwitchAccent = () => {
- const switches = document.querySelectorAll('input.switch')
- const [first] = switches
- if (!first) return
- // All switches share the same cascade; read from the first one.
- const color = getComputedStyle(first).getPropertyValue('accent-color').trim()
- if (!color || color === 'auto') return
- for (const el of switches) {
- el.style.setProperty('--switch-accent', color)
+ // The polyfill reads `accent-color` once at upgrade time and freezes the
+ // resolved color as `--switch-accent` in each element's inline style.
+ // Re-sync whenever the accent or color-mode changes so switches react live.
+ const syncSwitchAccent = () => {
+ const switches = document.querySelectorAll('input.switch')
+ const [first] = switches
+ if (!first) return
+ // All switches share the same cascade; read from the first one.
+ const color = getComputedStyle(first).getPropertyValue('accent-color').trim()
+ if (!color || color === 'auto') return
+ for (const el of switches) {
+ el.style.setProperty('--switch-accent', color)
+ }
}
- }
- new MutationObserver(syncSwitchAccent).observe(document.documentElement, {
- attributes: true,
- attributeFilter: ['style', 'class'],
- })
- }
+ new MutationObserver(syncSwitchAccent).observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['style', 'class'],
+ })
+ }
+ },
})
From a449b5eeff8326c2dc05f57adb36e6193ea4502b Mon Sep 17 00:00:00 2001
From: Thomas Steiner
Date: Sat, 14 Nov 2026 12:34:21 +0100
Subject: [PATCH 4/4] fix(ui): add explicit switch role to native toggle input
---
app/components/Settings/Toggle.client.vue | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue
index 4750e77a62..20b9fd0c12 100644
--- a/app/components/Settings/Toggle.client.vue
+++ b/app/components/Settings/Toggle.client.vue
@@ -56,6 +56,7 @@ const id = useId()