@@ -3,9 +3,8 @@ import TooltipApp from '~/components/Tooltip/App.vue'
33
44const props = withDefaults (
55 defineProps <{
6- label? : string
6+ label: string
77 description? : string
8- class? : string
98 justify? : ' between' | ' start'
109 tooltip? : string
1110 tooltipPosition? : ' top' | ' bottom' | ' left' | ' right'
@@ -20,45 +19,47 @@ const props = withDefaults(
2019)
2120
2221const checked = defineModel <boolean >({
23- default: false ,
22+ required: true ,
2423})
24+ const id = useId ()
2525 </script >
2626
2727<template >
28- <button
29- type =" button"
30- class =" w-full flex items-center gap-4 group focus-visible:outline-none py-1 -my-1"
31- :class =" [justify === 'start' ? 'justify-start' : 'justify-between', $props.class]"
32- role =" switch"
33- :aria-checked =" checked"
34- @click =" checked = !checked"
28+ <label
29+ :for =" id"
30+ class =" grid items-center gap-4 py-1 -my-1 grid-cols-[auto_1fr_auto]"
31+ :class =" [justify === 'start' ? 'justify-start' : '']"
32+ :style ="
33+ props.reverseOrder
34+ ? 'grid-template-areas: \'toggle . label-text\''
35+ : 'grid-template-areas: \'label-text . toggle\''
36+ "
3537 >
3638 <template v-if =" props .reverseOrder " >
37- <span
38- class =" inline-flex items-center h-6 w-11 shrink-0 rounded-full border p-0.25 transition-colors duration-200 shadow-sm ease-in-out motion-reduce:transition-none group-focus-visible:(outline-accent/70 outline-offset-2 outline-solid)"
39- :class ="
40- checked
41- ? 'bg-accent border-accent group-hover:bg-accent/80'
42- : 'bg-fg/50 border-fg/50 group-hover:bg-fg/70'
43- "
44- aria-hidden =" true"
45- >
46- <span
47- class =" block h-5 w-5 rounded-full bg-bg shadow-sm transition-transform duration-200 ease-in-out motion-reduce:transition-none"
48- />
49- </span >
39+ <input
40+ role =" switch"
41+ type =" checkbox"
42+ :id
43+ v-model =" checked"
44+ class =" toggle appearance-none h-6 w-11 rounded-full border border-fg relative shrink-0 bg-fg-subtle checked:bg-fg checked:border-fg focus-visible:(outline-2 outline-fg outline-offset-2) before:content-[''] before:absolute before:h-5 before:w-5 before:top-1px before:rounded-full before:bg-bg"
45+ style =" grid-area : toggle"
46+ />
5047 <TooltipApp
5148 v-if =" tooltip && label"
5249 :text =" tooltip"
5350 :position =" tooltipPosition ?? 'top'"
5451 :to =" tooltipTo"
5552 :offset =" tooltipOffset"
5653 >
57- <span class =" text-sm text-fg font-medium text-start" >
54+ <span class =" text-sm text-fg font-medium text-start" style = " grid-area : label-text " >
5855 {{ label }}
5956 </span >
6057 </TooltipApp >
61- <span v-else-if =" label" class =" text-sm text-fg font-medium text-start" >
58+ <span
59+ v-else-if =" label"
60+ class =" text-sm text-fg font-medium text-start"
61+ style =" grid-area : label-text"
62+ >
6263 {{ label }}
6364 </span >
6465 </template >
@@ -70,83 +71,109 @@ const checked = defineModel<boolean>({
7071 :to =" tooltipTo"
7172 :offset =" tooltipOffset"
7273 >
73- <span class =" text-sm text-fg font-medium text-start" >
74+ <span class =" text-sm text-fg font-medium text-start" style = " grid-area : label-text " >
7475 {{ label }}
7576 </span >
7677 </TooltipApp >
77- <span v-else-if =" label" class =" text-sm text-fg font-medium text-start" >
78- {{ label }}
79- </span >
8078 <span
81- class =" inline-flex items-center h-6 w-11 shrink-0 rounded-full border p-0.25 transition-colors duration-200 shadow-sm ease-in-out motion-reduce:transition-none group-focus-visible:(outline-accent/70 outline-offset-2 outline-solid)"
82- :class ="
83- checked
84- ? 'bg-accent border-accent group-hover:bg-accent/80'
85- : 'bg-fg/50 border-fg/50 group-hover:bg-fg/70'
86- "
87- aria-hidden =" true"
79+ v-else-if =" label"
80+ class =" text-sm text-fg font-medium text-start"
81+ style =" grid-area : label-text"
8882 >
89- <span
90- class =" block h-5 w-5 rounded-full bg-bg shadow-sm transition-transform duration-200 ease-in-out motion-reduce:transition-none"
91- />
83+ {{ label }}
9284 </span >
85+ <input
86+ role =" switch"
87+ type =" checkbox"
88+ :id
89+ v-model =" checked"
90+ class =" toggle appearance-none h-6 w-11 rounded-full border border-fg relative shrink-0 bg-fg-subtle checked:bg-fg checked:border-fg focus-visible:(outline-2 outline-fg outline-offset-2) before:content-[''] before:absolute before:h-5 before:w-5 before:top-1px before:rounded-full before:bg-bg"
91+ style =" grid-area : toggle; justify-self : end "
92+ />
9393 </template >
94- </button >
94+ </label >
9595 <p v-if =" description" class =" text-sm text-fg-muted mt-2" >
9696 {{ description }}
9797 </p >
9898</template >
9999
100100<style scoped>
101- /* Default order: label first, toggle last */
102- button [ aria-checked = ' false ' ] > span :last-of-type > span {
103- translate : 0 ;
101+ /* Thumb position: logical property for RTL support */
102+ .toggle ::before {
103+ inset-inline-start : 1 px ;
104104}
105- button [aria-checked = ' true' ] > span :last-of-type > span {
106- translate : calc (100% );
105+
106+ /* Track transition */
107+ .toggle {
108+ transition :
109+ background-color 200ms ease-in-out ,
110+ border-color 100ms ease-in-out ;
107111}
108- html [dir = ' rtl' ] button [aria-checked = ' true' ] > span :last-of-type > span {
109- translate : calc (-100% );
112+
113+ .toggle ::before {
114+ transition :
115+ background-color 200ms ease-in-out ,
116+ translate 200ms ease-in-out ;
110117}
111118
112- /* Reverse order: toggle first, label last */
113- button [ aria-checked = ' false ' ] > span :first-of-type > span {
114- translate : 0 ;
119+ /* Hover states */
120+ .toggle :hover:not ( :checked ) {
121+ background : var ( --fg-muted ) ;
115122}
116- button [aria-checked = ' true' ] > span :first-of-type > span {
117- translate : calc (100% );
123+
124+ .toggle :checked:hover {
125+ background : var (--fg-muted );
126+ border-color : var (--fg-muted );
118127}
119- html [dir = ' rtl' ] button [aria-checked = ' true' ] > span :first-of-type > span {
120- translate : calc (-100% );
128+
129+ /* RTL-aware checked thumb position */
130+ :dir (ltr ) .toggle :checked ::before {
131+ translate : 20px ;
132+ }
133+
134+ :dir (rtl ) .toggle :checked ::before {
135+ translate : -20px ;
121136}
122137
138+ @media (prefers-reduced-motion: reduce) {
139+ .toggle ,
140+ .toggle ::before {
141+ transition : none ;
142+ }
143+ }
144+
145+ /* Support forced colors */
123146@media (forced-colors: active) {
124- /* make toggle tracks and thumb visible in forced colors. */
125- button [role = ' switch' ] {
126- & > span :last-of-type,
127- & > span:first-of-type {
128- forced-color-adjust: none ;
129- }
130-
131- &[aria-checked = ' false' ] > span :last-of-type ,
132- &[aria-checked = ' false' ] > span :first-of-type {
133- background : Canvas;
134- border-color : CanvasText;
135-
136- & > span {
137- background : CanvasText;
138- }
139- }
140-
141- &[aria-checked = ' true' ] > span :last-of-type ,
142- &[aria-checked = ' true' ] > span :first-of-type {
143- background : Highlight ;
144- border-color : Highlight ;
145-
146- & > span {
147- background : HighlightText ;
148- }
149- }
147+ label > span {
148+ background : Canvas;
149+ color : Highlight ;
150+ forced-color-adjust : none ;
151+ }
152+
153+ label :has (.toggle :checked ) > span {
154+ background : Highlight ;
155+ color : Canvas;
156+ }
157+
158+ .toggle ::before {
159+ forced-color-adjust : none ;
160+ background-color : Highlight ;
161+ }
162+
163+ .toggle ,
164+ .toggle :hover {
165+ background : Canvas;
166+ border-color : CanvasText;
167+ }
168+
169+ .toggle :checked ,
170+ .toggle :checked:hover {
171+ background : Highlight ;
172+ border-color : CanvasText;
173+ }
174+
175+ .toggle :checked ::before {
176+ background : Canvas;
150177 }
151178}
152179 </style >
0 commit comments