Skip to content

fix(a11y): improve toggle switches#1782

Open
userquin wants to merge 3 commits intomainfrom
userquin/fix-a11y-toogle-switches
Open

fix(a11y): improve toggle switches#1782
userquin wants to merge 3 commits intomainfrom
userquin/fix-a11y-toogle-switches

Conversation

@userquin
Copy link
Member

@userquin userquin commented Mar 1, 2026

🔗 Linked issue

resolves #1758

🧭 Context

This PR change the component layout to add the checked icon using position absolute.

📚 Description

This PR includes:

  • change layout and styles for client component
  • cleanup server styles
image

With forced colors active and RTL locale (same for non RTL):

image

@vercel
Copy link

vercel bot commented Mar 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 1, 2026 2:55pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 1, 2026 2:55pm
npmx-lunaria Ignored Ignored Mar 1, 2026 2:55pm

Request Review

@codecov
Copy link

codecov bot commented Mar 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

The toggle component’s client markup was refactored from an inline input to a composed control: the label now contains a hidden checkbox input, a Track span and a Thumb/Check icon with grid-area placement and RTL-aware classes; related scoped styles were removed and CSS selectors were updated (including forced-colours/forced-colors handling) to target the new Track/Thumb structure. The server component’s props were extended with tooltip, tooltipPosition, tooltipTo and tooltipOffset, and the layout gap spacing was reduced.

Possibly related PRs

Suggested reviewers

  • danielroe
  • knowler
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset by explaining the layout and style changes made to the toggle component and the addition of a checked icon using absolute positioning.
Linked Issues check ✅ Passed The PR addresses the accessibility issue #1758 by implementing visual improvements to toggle switches with clearer on/off state distinction through the new Track and Thumb Icon structure.
Out of Scope Changes check ✅ Passed All changes in the PR are directly related to improving toggle switch accessibility as specified in issue #1758; no out-of-scope changes were detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch userquin/fix-a11y-toogle-switches

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/components/Settings/Toggle.client.vue (1)

128-134: Scope reduced-motion rules to toggle elements only.

Using span/span::after here is broader than necessary and may suppress transitions on unrelated spans added later in this component.

Refactor suggestion
 `@media` (prefers-reduced-motion: reduce) {
-  span,
-  span::after,
+  input + span,
+  input + span::after,
+  input + span + span,
   .i-lucide\:check {
     transition: none !important;
     animation: none !important;
   }
 }

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9485afa and ee1082e.

📒 Files selected for processing (2)
  • app/components/Settings/Toggle.client.vue
  • app/components/Settings/Toggle.server.vue

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/components/Settings/Toggle.client.vue (1)

150-156: Scope reduced-motion rules to toggle parts only

Line [151] currently targets every span in this component scope, which is broader than needed. Narrowing it to the switch elements avoids accidental side effects on non-toggle spans.

Suggested refactor
-@media (prefers-reduced-motion: reduce) {
-  span,
-  span::after,
-  .i-lucide\:check {
+@media (prefers-reduced-motion: reduce) {
+  input + span,
+  input + span::after,
+  .toggle-thumb-icon,
+  .i-lucide\:check {
     transition: none !important;
     animation: none !important;
   }
 }
-      <span
-        class="absolute top-[2px] start-[2px] h-5 w-5 rounded-full flex items-center justify-center transition-transform duration-200 ease-in-out pointer-events-none"
+      <span
+        class="toggle-thumb-icon absolute top-[2px] start-[2px] h-5 w-5 rounded-full flex items-center justify-center transition-transform duration-200 ease-in-out pointer-events-none"
         :class="checked ? 'translate-x-5 rtl:-translate-x-5' : 'translate-x-0'"
       >

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee1082e and b36e9c4.

📒 Files selected for processing (1)
  • app/components/Settings/Toggle.client.vue

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/components/Settings/Toggle.client.vue (2)

62-107: Remove duplicated reverse-order label block

Both props.reverseOrder and v-else branches render the same tooltip/label markup. Keeping a single block will reduce template complexity and future drift risk.

♻️ Proposed simplification
-    <template v-if="props.reverseOrder">
-      <TooltipApp
-        v-if="tooltip && label"
-        :text="tooltip"
-        :position="tooltipPosition ?? 'top'"
-        :to="tooltipTo"
-        :offset="tooltipOffset"
-      >
-        <span
-          class="toggle-label text-sm text-fg font-medium text-start"
-          style="grid-area: label-text"
-        >
-          {{ label }}
-        </span>
-      </TooltipApp>
-      <span
-        v-else-if="label"
-        class="toggle-label text-sm text-fg font-medium text-start"
-        style="grid-area: label-text"
-      >
-        {{ label }}
-      </span>
-    </template>
-    <template v-else>
-      <TooltipApp
-        v-if="tooltip && label"
-        :text="tooltip"
-        :position="tooltipPosition ?? 'top'"
-        :to="tooltipTo"
-        :offset="tooltipOffset"
-      >
-        <span
-          class="toggle-label text-sm text-fg font-medium text-start"
-          style="grid-area: label-text"
-        >
-          {{ label }}
-        </span>
-      </TooltipApp>
-      <span
-        v-else-if="label"
-        class="toggle-label text-sm text-fg font-medium text-start"
-        style="grid-area: label-text"
-      >
-        {{ label }}
-      </span>
-    </template>
+    <TooltipApp
+      v-if="tooltip && label"
+      :text="tooltip"
+      :position="tooltipPosition ?? 'top'"
+      :to="tooltipTo"
+      :offset="tooltipOffset"
+    >
+      <span
+        class="toggle-label text-sm text-fg font-medium text-start"
+        style="grid-area: label-text"
+      >
+        {{ label }}
+      </span>
+    </TooltipApp>
+    <span
+      v-else-if="label"
+      class="toggle-label text-sm text-fg font-medium text-start"
+      style="grid-area: label-text"
+    >
+      {{ label }}
+    </span>

46-52: Scope reduced-motion selectors to toggle elements only

The reduced-motion rule currently targets every span in the component. Narrowing this to toggle-specific elements will prevent accidental side effects as the template evolves.

🎯 Proposed selector scoping
-      <span
-        class="w-11 h-6 bg-fg-subtle peer-focus:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-fg peer-focus-visible:ring-offset-2 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-bg after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-fg transition-colors duration-200 ease-in-out"
-      ></span>
+      <span
+        class="toggle-track w-11 h-6 bg-fg-subtle peer-focus:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-fg peer-focus-visible:ring-offset-2 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-bg after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-fg transition-colors duration-200 ease-in-out"
+      ></span>
@@
-      <span
-        class="absolute top-[2px] start-[2px] h-5 w-5 rounded-full flex items-center justify-center transition-transform duration-200 ease-in-out pointer-events-none"
+      <span
+        class="toggle-thumb absolute top-[2px] start-[2px] h-5 w-5 rounded-full flex items-center justify-center transition-transform duration-200 ease-in-out pointer-events-none"
         :class="checked ? 'translate-x-5 rtl:-translate-x-5' : 'translate-x-0'"
       >
@@
 `@media` (prefers-reduced-motion: reduce) {
-  span,
-  span::after,
+  .toggle-track,
+  .toggle-track::after,
+  .toggle-thumb,
   .i-lucide\:check {
     transition: none !important;
     animation: none !important;
   }
 }

Also applies to: 156-162


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b36e9c4 and 77eb000.

📒 Files selected for processing (1)
  • app/components/Settings/Toggle.client.vue

@knowler knowler self-requested a review March 1, 2026 15:28
@danielroe danielroe requested a review from alexdln March 1, 2026 17:52
Copy link
Member

@knowler knowler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do a fuller review tomorrow, but I did notice the focus outlines are missing.


It would be very nice if we could avoid using the extra <span> elements. I imagine you went that route so that you could use icons, but I’m curious if there’s a way to use the icon information without the classes as it complicates the whole focus/activation situation. If we could use it in the content of one of the pseudo-elements of the <input> that would be ideal.

/* track */
input {}
/* thumb */
input::after {
  content: url(); /* checkmark data URI */
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve accessibility of toggle switches

2 participants