Skip to content

fix(a11y): improve toggle switches#1782

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

fix(a11y): improve toggle switches#1782
userquin wants to merge 5 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 5, 2026 9:27pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 5, 2026 9:27pm
npmx-lunaria Ignored Ignored Mar 5, 2026 9:27pm

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1bf97edf-686d-4e8b-a419-a1c0228c7c4a

📥 Commits

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

📒 Files selected for processing (1)
  • app/components/Settings/Toggle.client.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/components/Settings/Toggle.client.vue

📝 Walkthrough

Walkthrough

The client Toggle component markup was refactored: the label now wraps the checkbox input, Track span and Thumb/Check visuals with updated grid placement, RTL-aware classes and transition-friendly utility classes; the previous scoped toggle styles were removed and replaced by a new style block targeting the Track/Thumb structure and forced‑colour/reduced‑motion selectors. The server-side Toggle component’s props were extended to include tooltip, tooltipPosition, tooltipTo and tooltipOffset, and its layout gap was changed from gap-4 to gap-1.5. No exported signatures beyond props were removed.

Possibly related PRs

Suggested reviewers

  • knowler
  • danielroe
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive All changes are scoped to improving the toggle component's accessibility. The PR adds tooltip-related props to the server component, which may be tangentially related but supports the component's functionality. Clarify whether the new tooltip props (tooltip, tooltipPosition, tooltipTo, tooltipOffset) are required for issue #1758 or represent separate enhancements unrelated to the accessibility improvements.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description relates to the changeset, describing layout changes to the toggle component and referencing the linked issue #1758.
Linked Issues check ✅ Passed The PR implements improved visual distinction for toggle states with a checked icon using absolute positioning, addressing the accessibility requirement to increase contrast and graphical differentiation between on/off states [#1758].

✏️ 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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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 */
}

@alexdln
Copy link
Member

alexdln commented Mar 1, 2026

I am not sure the icon is a right change... It's a toggle, not a checkbox. And imo, the icon confuses the experience a bit more than it helps.

Regarding the implementation, if knowler will find it as important change - please add hovers (for the off and on states)

Some another option:
image
https://www.figma.com/design/ILKq2gR5RSyl03JgAnsimN/Untitled?node-id=0-1&t=CAk8uMrDcz0V74vo-1

P.S. To implement the message above, you can use something like: .class::after { @apply i-lucide:check; <your styles> }

@userquin
Copy link
Member Author

userquin commented Mar 1, 2026

I am not sure the icon is a right change... It's a toggle, not a checkbox. And imo, the icon confuses the experience a bit more than it helps.

https://m3.material.io/components/switch/overview

@knowler
Copy link
Member

knowler commented Mar 2, 2026

In #1782 (comment), @alexdln said:

Regarding the implementation, if knowler will find it as important change - please add hovers (for the off and on states)

Hover states are a good idea, too.


With regard to using an icons… switches are a difficult control to get right. Technically, they do pass something like WCAG’s “Use of color” success criterion, since the position of the thumb is enough to differentiate between the two states, but what’s still not immediately clear without colour is which position is on or off. That’s where icons come in. It seems like usually, the icon isn’t placed on the thumb, but within the track (see “Toggle” on Components Gallery or Open UI’s research for “Switch” or try the “On/Off Labels” setting in the “Display & Text Size” accessibility settings on iOS if you have such a device). Even then, I find icons don’t fully clear up the confusion (e.g. does the icon mean that’s the state it will be in if I toggle it?). So, I don’t have any strong opinions about the necessity of including them. One of those things that could go either way.

Some resources as I find them:

@userquin
Copy link
Member Author

userquin commented Mar 2, 2026

We can use "segmented buttons" (from md3 expressive) to solve the issue with the meaning (with enabled/disabled texts): https://m3.material.io/components/segmented-buttons/overview

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

3 participants