From 21e2b7de0062a3c1dca7209ca48b736dd9a36c84 Mon Sep 17 00:00:00 2001 From: "Khai Shern, Toh" <55418374+Leos-Khai@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:36:12 +0800 Subject: [PATCH] fix(web): switches announce their real state to screen readers (#11580) Co-authored-by: Claude Opus 5 (1M context) --- apps/web/src/components/ui/switch.test.tsx | 20 ++++++++++++++++++++ apps/web/src/components/ui/switch.tsx | 5 ++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/ui/switch.test.tsx diff --git a/apps/web/src/components/ui/switch.test.tsx b/apps/web/src/components/ui/switch.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b63fa79e38498040170371e251dbb858e87aaf7b --- /dev/null +++ b/apps/web/src/components/ui/switch.test.tsx @@ -0,0 +1,20 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vite-plus/test"; + +import { Switch } from "./switch"; + +function renderSwitchRoot(props: Partial[0]>) { + const html = renderToStaticMarkup(); + return html.match(/]*role="switch"[^>]*>/)?.[0] ?? ""; +} + +describe("Switch accessibility", () => { + it("exposes the checked state to assistive tech", () => { + expect(renderSwitchRoot({ checked: true })).toContain('aria-checked="true"'); + expect(renderSwitchRoot({ checked: false })).toContain('aria-checked="false"'); + }); + + it("exposes the mixed state", () => { + expect(renderSwitchRoot({ checked: false, mixed: true })).toContain('aria-checked="mixed"'); + }); +}); diff --git a/apps/web/src/components/ui/switch.tsx b/apps/web/src/components/ui/switch.tsx index 33114c5512bee62aecb7bd1f47b049654ab31220..e6bf20aa64c7ed2e18639831a441b73b540bfe58 100644 --- a/apps/web/src/components/ui/switch.tsx +++ b/apps/web/src/components/ui/switch.tsx @@ -27,7 +27,10 @@ function Switch({ data-size={size} data-slot="switch" data-mixed={mixed ? "" : undefined} - aria-checked={mixed ? "mixed" : undefined} + // Base UI copies every key we pass, even `undefined`, over its own + // aria-checked. Only pass the attribute when mixed so the real state + // survives for screen readers. + {...(mixed ? { "aria-checked": "mixed" as const } : {})} {...props} >