@slotted/styles 0.0.0-stage → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/package.json +276 -3
  4. package/src/accordion/accordion.css +96 -0
  5. package/src/accordion/accordion.tokens.json +14 -0
  6. package/src/action-bar/action-bar.css +455 -0
  7. package/src/action-bar/action-bar.tokens.json +50 -0
  8. package/src/action-sheet/action-sheet.css +462 -0
  9. package/src/action-sheet/action-sheet.tokens.json +59 -0
  10. package/src/alert/alert.css +764 -0
  11. package/src/alert/alert.tokens.json +77 -0
  12. package/src/app-shell/app-shell.css +1899 -0
  13. package/src/app-shell/app-shell.tokens.json +88 -0
  14. package/src/aspect-ratio/aspect-ratio.css +131 -0
  15. package/src/aspect-ratio/aspect-ratio.tokens.json +1 -0
  16. package/src/attachment/attachment.css +906 -0
  17. package/src/attachment/attachment.tokens.json +85 -0
  18. package/src/avatar/avatar.css +598 -0
  19. package/src/avatar/avatar.tokens.json +64 -0
  20. package/src/avatar-group/avatar-group.css +268 -0
  21. package/src/avatar-group/avatar-group.tokens.json +16 -0
  22. package/src/badge/badge.css +462 -0
  23. package/src/badge/badge.tokens.json +58 -0
  24. package/src/breadcrumb/breadcrumb.css +441 -0
  25. package/src/breadcrumb/breadcrumb.tokens.json +50 -0
  26. package/src/button/button.css +754 -0
  27. package/src/button/button.tokens.json +72 -0
  28. package/src/button-group/button-group.css +455 -0
  29. package/src/button-group/button-group.tokens.json +11 -0
  30. package/src/calendar/calendar.css +846 -0
  31. package/src/calendar/calendar.tokens.json +67 -0
  32. package/src/card/card.css +602 -0
  33. package/src/card/card.tokens.json +68 -0
  34. package/src/carousel/carousel.css +860 -0
  35. package/src/carousel/carousel.tokens.json +62 -0
  36. package/src/cascader/cascader.css +1320 -0
  37. package/src/cascader/cascader.tokens.json +103 -0
  38. package/src/chart/chart.css +1473 -0
  39. package/src/chart/chart.tokens.json +107 -0
  40. package/src/checkbox/checkbox.css +1011 -0
  41. package/src/checkbox/checkbox.tokens.json +102 -0
  42. package/src/checkbox-group/checkbox-group.css +70 -0
  43. package/src/checkbox-group/checkbox-group.tokens.json +6 -0
  44. package/src/code-block/code-block.css +555 -0
  45. package/src/code-block/code-block.tokens.json +69 -0
  46. package/src/collapsible/collapsible.css +546 -0
  47. package/src/collapsible/collapsible.tokens.json +55 -0
  48. package/src/color-picker/color-picker.css +607 -0
  49. package/src/color-picker/color-picker.tokens.json +58 -0
  50. package/src/column-manager/column-manager.css +452 -0
  51. package/src/column-manager/column-manager.tokens.json +43 -0
  52. package/src/combobox/combobox.css +1048 -0
  53. package/src/combobox/combobox.tokens.json +112 -0
  54. package/src/command-palette/command-palette.css +1000 -0
  55. package/src/command-palette/command-palette.tokens.json +75 -0
  56. package/src/container/container.css +198 -0
  57. package/src/container/container.tokens.json +18 -0
  58. package/src/context-menu/context-menu.css +104 -0
  59. package/src/context-menu/context-menu.tokens.json +11 -0
  60. package/src/currency-field/currency-field.css +787 -0
  61. package/src/currency-field/currency-field.tokens.json +80 -0
  62. package/src/dashboard-grid/dashboard-grid.css +748 -0
  63. package/src/dashboard-grid/dashboard-grid.tokens.json +67 -0
  64. package/src/data-grid/data-grid.css +2484 -0
  65. package/src/data-grid/data-grid.tokens.json +100 -0
  66. package/src/data-list/data-list.css +455 -0
  67. package/src/data-list/data-list.tokens.json +35 -0
  68. package/src/date-field/date-field.css +560 -0
  69. package/src/date-field/date-field.tokens.json +80 -0
  70. package/src/date-picker/date-picker.css +420 -0
  71. package/src/date-picker/date-picker.tokens.json +49 -0
  72. package/src/date-range-picker/date-range-picker.css +983 -0
  73. package/src/date-range-picker/date-range-picker.tokens.json +109 -0
  74. package/src/datetime-field/datetime-field.css +850 -0
  75. package/src/datetime-field/datetime-field.tokens.json +91 -0
  76. package/src/datetime-picker/datetime-picker.css +843 -0
  77. package/src/datetime-picker/datetime-picker.tokens.json +78 -0
  78. package/src/datetime-range-picker/datetime-range-picker.css +1375 -0
  79. package/src/datetime-range-picker/datetime-range-picker.tokens.json +135 -0
  80. package/src/description-list/description-list.css +675 -0
  81. package/src/description-list/description-list.tokens.json +66 -0
  82. package/src/detail-panel/detail-panel.css +329 -0
  83. package/src/detail-panel/detail-panel.tokens.json +33 -0
  84. package/src/dialog/dialog.css +968 -0
  85. package/src/dialog/dialog.tokens.json +66 -0
  86. package/src/divider/divider.css +131 -0
  87. package/src/divider/divider.tokens.json +18 -0
  88. package/src/drawer/drawer.css +1303 -0
  89. package/src/drawer/drawer.tokens.json +75 -0
  90. package/src/entity-picker/entity-picker.css +1367 -0
  91. package/src/entity-picker/entity-picker.tokens.json +118 -0
  92. package/src/event-calendar/event-calendar.css +2148 -0
  93. package/src/event-calendar/event-calendar.tokens.json +87 -0
  94. package/src/field/field.css +707 -0
  95. package/src/field/field.tokens.json +58 -0
  96. package/src/field-group/field-group.css +226 -0
  97. package/src/field-group/field-group.tokens.json +33 -0
  98. package/src/filter-bar/filter-bar.css +1306 -0
  99. package/src/filter-bar/filter-bar.tokens.json +78 -0
  100. package/src/form/form.css +218 -0
  101. package/src/form/form.tokens.json +32 -0
  102. package/src/fullscreen/fullscreen.css +376 -0
  103. package/src/fullscreen/fullscreen.tokens.json +50 -0
  104. package/src/grid/grid.css +173 -0
  105. package/src/grid/grid.tokens.json +20 -0
  106. package/src/highlight/highlight.css +135 -0
  107. package/src/highlight/highlight.tokens.json +16 -0
  108. package/src/hover-card/hover-card.css +93 -0
  109. package/src/hover-card/hover-card.tokens.json +17 -0
  110. package/src/icon/icon.css +136 -0
  111. package/src/icon/icon.tokens.json +14 -0
  112. package/src/image/image.css +449 -0
  113. package/src/image/image.tokens.json +27 -0
  114. package/src/import-export-center/import-export-center.css +1175 -0
  115. package/src/import-export-center/import-export-center.tokens.json +84 -0
  116. package/src/inline/inline.css +149 -0
  117. package/src/inline/inline.tokens.json +14 -0
  118. package/src/inline-message/inline-message.css +396 -0
  119. package/src/inline-message/inline-message.tokens.json +19 -0
  120. package/src/kbd/kbd.css +490 -0
  121. package/src/kbd/kbd.tokens.json +40 -0
  122. package/src/link/link.css +237 -0
  123. package/src/link/link.tokens.json +25 -0
  124. package/src/list/list.css +240 -0
  125. package/src/list/list.tokens.json +42 -0
  126. package/src/list-item/list-item.css +937 -0
  127. package/src/list-item/list-item.tokens.json +114 -0
  128. package/src/listbox/listbox.css +674 -0
  129. package/src/listbox/listbox.tokens.json +72 -0
  130. package/src/loading-overlay/loading-overlay.css +730 -0
  131. package/src/loading-overlay/loading-overlay.tokens.json +48 -0
  132. package/src/markdown/markdown.css +168 -0
  133. package/src/markdown/markdown.tokens.json +18 -0
  134. package/src/menu/menu.css +817 -0
  135. package/src/menu/menu.tokens.json +78 -0
  136. package/src/menubar/menubar.css +170 -0
  137. package/src/menubar/menubar.tokens.json +32 -0
  138. package/src/message/message.css +1453 -0
  139. package/src/message/message.tokens.json +85 -0
  140. package/src/message-scroller/message-scroller.css +371 -0
  141. package/src/message-scroller/message-scroller.tokens.json +47 -0
  142. package/src/multi-select/multi-select.css +1427 -0
  143. package/src/multi-select/multi-select.tokens.json +114 -0
  144. package/src/navigation-menu/navigation-menu.css +1029 -0
  145. package/src/navigation-menu/navigation-menu.tokens.json +70 -0
  146. package/src/notification-center/notification-center.css +856 -0
  147. package/src/notification-center/notification-center.tokens.json +84 -0
  148. package/src/number-field/number-field.css +794 -0
  149. package/src/number-field/number-field.tokens.json +80 -0
  150. package/src/otp-input/otp-input.css +525 -0
  151. package/src/otp-input/otp-input.tokens.json +69 -0
  152. package/src/page/page.css +179 -0
  153. package/src/page/page.tokens.json +20 -0
  154. package/src/page-header/page-header.css +1181 -0
  155. package/src/page-header/page-header.tokens.json +71 -0
  156. package/src/pagination/pagination.css +500 -0
  157. package/src/pagination/pagination.tokens.json +56 -0
  158. package/src/popover/popover.css +526 -0
  159. package/src/popover/popover.tokens.json +60 -0
  160. package/src/progress-indicator/progress-indicator.css +1584 -0
  161. package/src/progress-indicator/progress-indicator.tokens.json +50 -0
  162. package/src/prompt-composer/prompt-composer.css +680 -0
  163. package/src/prompt-composer/prompt-composer.tokens.json +99 -0
  164. package/src/prose/prose.css +733 -0
  165. package/src/prose/prose.tokens.json +79 -0
  166. package/src/radio-group/radio-group.css +778 -0
  167. package/src/radio-group/radio-group.tokens.json +103 -0
  168. package/src/rating/rating.css +1071 -0
  169. package/src/rating/rating.tokens.json +88 -0
  170. package/src/result-state/result-state.css +980 -0
  171. package/src/result-state/result-state.tokens.json +63 -0
  172. package/src/runtime-properties.mjs +321 -0
  173. package/src/saved-views/saved-views.css +394 -0
  174. package/src/saved-views/saved-views.tokens.json +41 -0
  175. package/src/scroll-area/scroll-area.css +481 -0
  176. package/src/scroll-area/scroll-area.tokens.json +15 -0
  177. package/src/section/section.css +177 -0
  178. package/src/section/section.tokens.json +39 -0
  179. package/src/segmented-control/segmented-control.css +564 -0
  180. package/src/segmented-control/segmented-control.tokens.json +67 -0
  181. package/src/select/select.css +1593 -0
  182. package/src/select/select.tokens.json +110 -0
  183. package/src/side-navigation/side-navigation.css +1096 -0
  184. package/src/side-navigation/side-navigation.tokens.json +87 -0
  185. package/src/skeleton/skeleton.css +431 -0
  186. package/src/skeleton/skeleton.tokens.json +29 -0
  187. package/src/slider/slider.css +792 -0
  188. package/src/slider/slider.tokens.json +75 -0
  189. package/src/sparkline/sparkline.css +282 -0
  190. package/src/sparkline/sparkline.tokens.json +45 -0
  191. package/src/spinner/spinner.css +224 -0
  192. package/src/spinner/spinner.tokens.json +8 -0
  193. package/src/split-button/split-button.css +253 -0
  194. package/src/split-button/split-button.tokens.json +12 -0
  195. package/src/splitter/splitter.css +336 -0
  196. package/src/splitter/splitter.tokens.json +24 -0
  197. package/src/stack/stack.css +128 -0
  198. package/src/stack/stack.tokens.json +14 -0
  199. package/src/stat-card/stat-card.css +641 -0
  200. package/src/stat-card/stat-card.tokens.json +95 -0
  201. package/src/status-indicator/status-indicator.css +481 -0
  202. package/src/status-indicator/status-indicator.tokens.json +38 -0
  203. package/src/stepper/stepper.css +629 -0
  204. package/src/stepper/stepper.tokens.json +72 -0
  205. package/src/switch/switch.css +703 -0
  206. package/src/switch/switch.tokens.json +75 -0
  207. package/src/table/table.css +1482 -0
  208. package/src/table/table.tokens.json +69 -0
  209. package/src/table-of-contents/table-of-contents.css +533 -0
  210. package/src/table-of-contents/table-of-contents.tokens.json +53 -0
  211. package/src/tabs/tabs.css +998 -0
  212. package/src/tabs/tabs.tokens.json +82 -0
  213. package/src/tag/tag.css +768 -0
  214. package/src/tag/tag.tokens.json +85 -0
  215. package/src/tags-input/tags-input.css +757 -0
  216. package/src/tags-input/tags-input.tokens.json +84 -0
  217. package/src/text-field/text-field.css +900 -0
  218. package/src/text-field/text-field.tokens.json +87 -0
  219. package/src/textarea/textarea.css +365 -0
  220. package/src/textarea/textarea.tokens.json +59 -0
  221. package/src/time-field/time-field.css +719 -0
  222. package/src/time-field/time-field.tokens.json +86 -0
  223. package/src/time-picker/time-picker.css +665 -0
  224. package/src/time-picker/time-picker.tokens.json +66 -0
  225. package/src/time-range-picker/time-range-picker.css +1287 -0
  226. package/src/time-range-picker/time-range-picker.tokens.json +134 -0
  227. package/src/timeline/timeline.css +1596 -0
  228. package/src/timeline/timeline.tokens.json +96 -0
  229. package/src/toast/toast.css +943 -0
  230. package/src/toast/toast.tokens.json +90 -0
  231. package/src/toggle/toggle.css +290 -0
  232. package/src/toggle/toggle.tokens.json +54 -0
  233. package/src/toggle-group/toggle-group.css +463 -0
  234. package/src/toggle-group/toggle-group.tokens.json +54 -0
  235. package/src/toolbar/toolbar.css +434 -0
  236. package/src/toolbar/toolbar.tokens.json +49 -0
  237. package/src/tooltip/tooltip.css +368 -0
  238. package/src/tooltip/tooltip.tokens.json +39 -0
  239. package/src/top-navigation/top-navigation.css +1287 -0
  240. package/src/top-navigation/top-navigation.tokens.json +91 -0
  241. package/src/tour/tour.css +680 -0
  242. package/src/tour/tour.tokens.json +67 -0
  243. package/src/transfer/transfer.css +571 -0
  244. package/src/transfer/transfer.tokens.json +49 -0
  245. package/src/tree/tree.css +1000 -0
  246. package/src/tree/tree.tokens.json +80 -0
  247. package/src/typography/typography.css +211 -0
  248. package/src/typography/typography.tokens.json +31 -0
  249. package/src/upload/upload.css +1706 -0
  250. package/src/upload/upload.tokens.json +123 -0
  251. package/src/visually-hidden/visually-hidden.css +52 -0
  252. package/src/visually-hidden/visually-hidden.tokens.json +1 -0
  253. package/src/wizard/wizard.css +740 -0
  254. package/src/wizard/wizard.tokens.json +63 -0
@@ -0,0 +1,703 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── The binary setting that takes effect immediately ───────────────────
4
+ *
5
+ * The control is an `input[type=checkbox][role=switch]` and the pill is
6
+ * painted around it. ARIA 1.2 declares the `switch` role **Children
7
+ * Presentational**, so the role belongs on the void `input` and never on the
8
+ * box: a root carrying it would fold the label and the description beside it
9
+ * into the control's own accessible name.
10
+ *
11
+ * The anatomy is the one `checkbox` settled one item earlier in this wave,
12
+ * and the metrics below are its metrics: a two-column grid, the label in the
13
+ * second column pointing at the control through `for`, and the description
14
+ * under the label. Two binary controls in one list have to line up, and the
15
+ * way to guarantee that is to derive both from the same numbers — including
16
+ * where the target comes from, which in both is the control and not the row
17
+ * (SW2).
18
+ *
19
+ * Every rule keys on `data-ui` and `data-part`. The checked, disabled and
20
+ * read-only states are read from the element as well as from the attribute
21
+ * the binding mirrors, because a control can be disabled by something that
22
+ * never told it so.
23
+ */
24
+ [data-ui='switch'][data-part='root'] {
25
+ /**
26
+ * The thumb is the control glyph, which is the scale the theme restates
27
+ * per density — so a switch in a dense table shrinks whole rather than in
28
+ * parts. Everything else is a proportion of it: a pill two thumbs and
29
+ * three gutters wide, whose thumb travels exactly its own diameter plus
30
+ * one gutter, so the same air is left at both ends in both states.
31
+ */
32
+ --_thumb-size: var(--slotted-control-icon-size-md, 16px);
33
+ --_gutter: var(--slotted-space-1, 0.125rem);
34
+ --_border: var(--slotted-control-border-width, 1px);
35
+ --_track-block: calc(var(--_thumb-size) + (var(--_gutter) + var(--_border)) * 2);
36
+ --_track-inline: var(
37
+ --slotted-switch-track-inline-size,
38
+ calc(var(--_thumb-size) * 2 + var(--_gutter) * 3 + var(--_border) * 2)
39
+ );
40
+ --_travel: calc(var(--_thumb-size) + var(--_gutter));
41
+ --_font-size: var(--slotted-control-font-size-md, 13px);
42
+ --_gap: var(--slotted-control-label-gap-md, 12px);
43
+ --_line-height: var(--slotted-switch-line-height, var(--slotted-line-height-md, 1.5));
44
+ /**
45
+ * The height the label's line is missing before it is as tall as the pill,
46
+ * split above and below its text, so the text reads centred on the pill
47
+ * rather than sitting at the top of a row the pill made.
48
+ *
49
+ * It was the height a one-line label was missing before it was a WCAG 2.2
50
+ * target, which made a row of Switch four and a half pixels taller than
51
+ * the row of Checkbox beside it in the same form — 28.5px against 19.5 at
52
+ * `md` — for a target the control above the pill already carries. CK3 took
53
+ * that model out of `checkbox` and `radio-group`; this is the same removal
54
+ * (SW2). A row is the taller of its label's line and its pill.
55
+ */
56
+ --_label-slack: max(0px, (var(--_track-block) - var(--_font-size) * var(--_line-height)) / 2);
57
+ /**
58
+ * The distance from the top of the row to the top of the pill, so the pill
59
+ * sits on the first line of a label that wraps to three rather than in the
60
+ * middle of the block. The pill is taller than a line box, so this is
61
+ * usually the slack less a pixel; `max()` keeps it from going negative at
62
+ * a step where the pill is taller still.
63
+ */
64
+ --_lift: max(
65
+ 0px,
66
+ calc(
67
+ var(--_label-slack) + (var(--_font-size) * var(--_line-height) - var(--_track-block)) / 2
68
+ )
69
+ );
70
+ /** The box the pointer presses: the pill, or the WCAG 2.2 minimum where
71
+ the pill is shorter than it. */
72
+ --_target-block: max(var(--_track-block), var(--slotted-target-min, 24px));
73
+ /** The distance from the label's text to the description under it. */
74
+ --_description-gap: max(0px, var(--slotted-space-3, 0.375rem) - var(--_label-slack));
75
+
76
+ /* The rows are as tall as they read. Stretched, the description drifted
77
+ away from the label it belongs to; the surplus is the container's. */
78
+ align-content: start;
79
+ color: var(--slotted-switch-color, var(--slotted-text, CanvasText));
80
+ display: grid;
81
+ font-family: var(--slotted-control-font-family, system-ui, sans-serif);
82
+ font-size: var(--_font-size);
83
+ grid-template-columns: auto minmax(0, 1fr);
84
+ line-height: var(--_line-height);
85
+ }
86
+
87
+ /**
88
+ * The control. It gives up every pixel of its own appearance and keeps
89
+ * everything that makes it a control: the state, the identifier, the name,
90
+ * the tab stop, Space, the label association and form submission.
91
+ *
92
+ * It is transparent rather than clipped away, because a control removed from
93
+ * the layout takes its hit area with it — and this one *is* the hit area
94
+ * over the pill. It is the WCAG minimum in both axes wherever the pill is
95
+ * smaller than it, and it overhangs the pill by the difference rather than
96
+ * pushing the row open: the two negative margins take its own overhang back
97
+ * out of what it contributes to the row, so a Switch row is the height of
98
+ * its pill and its label and never of its target (SW2). The overhang is on
99
+ * a transparent box, so nothing about it is visible.
100
+ */
101
+ [data-ui='switch'] > [data-part='control'] {
102
+ align-self: start;
103
+ appearance: none;
104
+ background-color: transparent;
105
+ block-size: var(--_target-block);
106
+ border: 0;
107
+ cursor: pointer;
108
+ grid-area: 1 / 1;
109
+ inline-size: var(--_track-inline);
110
+ /* It follows the pill onto the label's first line, and gives back what it
111
+ overhangs at both ends. */
112
+ margin-block-end: calc((var(--_track-block) - var(--_target-block)) / 2);
113
+ margin-block-start: calc(var(--_lift) + (var(--_track-block) - var(--_target-block)) / 2);
114
+ margin-inline: 0;
115
+ opacity: 0;
116
+ /* The ring is drawn on the track, so the control never draws a second. */
117
+ outline: none;
118
+ padding: 0;
119
+ /* No tap delay waiting for a second tap a switch cannot use. */
120
+ touch-action: manipulation;
121
+ /**
122
+ * Above the paint, and this is not optional. The track shares this grid
123
+ * cell and comes later in the DOM, so it paints over the control: a
124
+ * pointer press landed on the track and the switch answered nothing, and
125
+ * only the label toggled it. That is the defect a reviewer found by
126
+ * pressing the pill rather than by reading the rule.
127
+ *
128
+ * It is a local stacking order rather than a step on the z-index scale:
129
+ * that scale ranks application layers — sticky, overlay, popover, toast —
130
+ * and this ranks two elements inside one control.
131
+ */
132
+ z-index: 1;
133
+ -webkit-tap-highlight-color: transparent;
134
+ }
135
+
136
+ /**
137
+ * The track. It carries the ground, the edge and the ring, and it is the
138
+ * containing block the thumb travels inside. It is lifted onto the label's
139
+ * first line rather than centred on the label, because `align-items: center`
140
+ * on a label that wraps to three lines puts the pill beside the second one.
141
+ */
142
+ [data-ui='switch'] > [data-part='track'] {
143
+ background-color: var(
144
+ --slotted-switch-track-background,
145
+ var(--slotted-tone-secondary-solid, ButtonFace)
146
+ );
147
+ block-size: var(--_track-block);
148
+ border: var(--_border) solid
149
+ var(--slotted-switch-border-color, var(--slotted-control-border-color, ButtonBorder));
150
+ border-radius: var(--slotted-switch-radius, var(--slotted-radius-full, 9999px));
151
+ box-sizing: border-box;
152
+ grid-area: 1 / 1;
153
+ inline-size: var(--_track-inline);
154
+ margin-block-start: var(--_lift);
155
+ /* The pointer belongs to the control above it, not to the paint. */
156
+ pointer-events: none;
157
+ position: relative;
158
+ transition:
159
+ background-color var(--slotted-control-transition-duration)
160
+ var(--slotted-control-transition-easing),
161
+ border-color var(--slotted-control-transition-duration)
162
+ var(--slotted-control-transition-easing);
163
+ }
164
+
165
+ /**
166
+ * The thumb, and the whole of the family's motion.
167
+ *
168
+ * It travels on `inset-inline-start` rather than on a translation, so a
169
+ * right-to-left document mirrors the movement with no direction-specific
170
+ * rule at all — the axis is the writing direction's, not the screen's.
171
+ */
172
+ [data-ui='switch'] > [data-part='track'] > [data-part='thumb'] {
173
+ /* A flex box, so the mark's icon is centred in the knob as a box and not
174
+ set on a line of text. */
175
+ align-items: center;
176
+ /**
177
+ * One knob, in both states and all three schemes. `on-solid` is the ink the
178
+ * theme guarantees against a solid tone, and both tracks are solid tones —
179
+ * so the thumb is white wherever the theme is legible. Reading `surface`
180
+ * here instead inverted the relationship between scheme and state: a white
181
+ * knob on a light track in the light scheme, and a near-black one on a
182
+ * lighter track in the dark scheme, which is two components rather than one.
183
+ */
184
+ background-color: var(--slotted-switch-thumb-background, var(--slotted-tone-accent-on-solid));
185
+ /**
186
+ * The thumb keeps an edge in every state. A thumb separated from its track
187
+ * by colour alone disappears in the `high-contrast` scheme, where the
188
+ * unchecked track is white, and under forced colours, where it is Canvas.
189
+ */
190
+ border: var(--_border) solid
191
+ var(--slotted-switch-thumb-border-color, var(--slotted-control-border-color, ButtonBorder));
192
+ border-radius: var(--slotted-radius-full, 9999px);
193
+ block-size: var(--_thumb-size);
194
+ /**
195
+ * The shadow every framed control carries, on the one part of this family
196
+ * that is meant to look raised. It is barely visible on purpose: on a
197
+ * light unchecked track the knob and the ground are two steps apart, and
198
+ * without it the knob rests on its own hairline border alone. The theme
199
+ * withdraws it in `high-contrast` and under forced colours, where the
200
+ * border is doing the work instead.
201
+ */
202
+ box-shadow: var(--slotted-switch-thumb-shadow, var(--slotted-control-shadow, none));
203
+ box-sizing: border-box;
204
+ display: flex;
205
+ inline-size: var(--_thumb-size);
206
+ inset-block-start: var(--_gutter);
207
+ inset-inline-start: var(--_gutter);
208
+ justify-content: center;
209
+ position: absolute;
210
+ transition:
211
+ inset-inline-start var(--slotted-control-transition-duration)
212
+ var(--slotted-control-transition-easing),
213
+ background-color var(--slotted-control-transition-duration)
214
+ var(--slotted-control-transition-easing),
215
+ border-color var(--slotted-control-transition-duration)
216
+ var(--slotted-control-transition-easing);
217
+ }
218
+
219
+ /**
220
+ * The mark. `docs/component-features.md` asks for the state to be
221
+ * perceptible beyond colour *and* beyond position, and this is what supplies
222
+ * it: the check the binding renders in the knob as the `Glyph` element,
223
+ * never a character. `indicator="none"` is the binding rendering none.
224
+ *
225
+ * It is always in the DOM and fades in, so the eye follows one control
226
+ * changing rather than two marks swapping places. Off is the absence of the
227
+ * mark, which is the same statement a checkbox makes.
228
+ */
229
+ [data-ui='switch'] > [data-part='track'] > [data-part='thumb'] > [data-ui='icon'] {
230
+ /**
231
+ * Seventy per cent of the knob rather than all of it. The glyph is drawn
232
+ * on a 24-unit grid with a 3-unit stroke, so filling the knob the check
233
+ * read as a checkbox on a pill.
234
+ */
235
+ block-size: 70%;
236
+ color: var(--slotted-switch-mark-color, var(--slotted-tone-accent-solid));
237
+ inline-size: 70%;
238
+ opacity: 0;
239
+ transition: opacity var(--slotted-control-transition-duration)
240
+ var(--slotted-control-transition-easing);
241
+ }
242
+
243
+ /* ── The label ────────────────────────────────────────────────────────
244
+ *
245
+ * The label is the control's own element and it names it through `for`, so a
246
+ * description below it can be read without toggling anything.
247
+ *
248
+ * It does not carry the target. The pill is 22 pixels tall at `md` and less
249
+ * at the tighter densities, and what makes up the difference is the control
250
+ * stretched over the pill, which is a transparent box with a minimum of its
251
+ * own — the correction `docs/token-foundation.md` asks the family for. The
252
+ * padding here is optical: it centres the words on the pill.
253
+ */
254
+ [data-ui='switch'] > [data-part='label'] {
255
+ cursor: pointer;
256
+ grid-column: 2;
257
+ grid-row: 1;
258
+ /** A label that is one unbroken token — an id, a hash, a URL — breaks. */
259
+ overflow-wrap: anywhere;
260
+ padding-block: var(--_label-slack);
261
+ }
262
+
263
+ /**
264
+ * The gap between the pill and the text is the grid's, opened only beside a
265
+ * label, and not the label's padding: a padding is laid out in the label's
266
+ * own direction, and a label in Arabic inside a row in Portuguese took the
267
+ * gap on its far side and set its text against the pill.
268
+ *
269
+ * The padding was also what made the gap toggle the setting. The control
270
+ * takes that over: it grows by the gap towards the text and gives the growth
271
+ * back as a negative margin, as it gives back its block overhang, so the row
272
+ * is no wider and a press between the pill and the words still lands on it.
273
+ */
274
+ [data-ui='switch'][data-part='root']:has(> [data-part='label']) {
275
+ column-gap: var(--_gap);
276
+ }
277
+
278
+ /* Beside the pill, the label is as wide as its words: stretched across the
279
+ column, a label in Arabic in a row in Portuguese aligned its text to its
280
+ own start and set it at the far end of the row, away from the pill. */
281
+ [data-ui='switch'] > [data-part='label'] {
282
+ justify-self: start;
283
+ }
284
+
285
+ [data-ui='switch'][data-part='root']:has(> [data-part='label']) > [data-part='control'] {
286
+ inline-size: calc(var(--_track-inline) + var(--_gap));
287
+ margin-inline-end: calc(-1 * var(--_gap));
288
+ }
289
+
290
+ /**
291
+ * Aligned under the label rather than under the pill, and never inside the
292
+ * label: inside, it would join the accessible name, and a paragraph of help
293
+ * text that toggles a setting when it is clicked is a paragraph nobody reads.
294
+ */
295
+ [data-ui='switch'] > [data-part='description'] {
296
+ color: var(--slotted-switch-description-color, var(--slotted-text-muted, GrayText));
297
+ font-size: var(--slotted-switch-description-font-size, var(--slotted-font-size-xs, 0.75rem));
298
+ grid-column: 2;
299
+ grid-row: 2;
300
+ line-height: var(--slotted-line-height-xs, 1.4);
301
+ margin: 0;
302
+ overflow-wrap: anywhere;
303
+ padding-block-start: var(--_description-gap);
304
+ }
305
+
306
+ /* ── Placement ────────────────────────────────────────────────────────
307
+ *
308
+ * The pill on the other side of its label, which is the settings row every
309
+ * product writes: the label takes the free width and the control sits at the
310
+ * end. Two columns swap and the column gap stays between them, so the
311
+ * distance is the same number in both arrangements and in both writing
312
+ * directions.
313
+ */
314
+
315
+ [data-ui='switch'][data-part='root'][data-label-placement='start'] {
316
+ /**
317
+ * `1fr` and not `max-content`, for the reason `checkbox` records: sizing
318
+ * the first column from each row's own label ends a list with the controls
319
+ * at as many different distances from the edge as there are rows.
320
+ */
321
+ grid-template-columns: minmax(0, 1fr) auto;
322
+ }
323
+
324
+ [data-ui='switch'][data-part='root'][data-label-placement='start'] > [data-part='control'],
325
+ [data-ui='switch'][data-part='root'][data-label-placement='start'] > [data-part='track'] {
326
+ grid-area: 1 / 2;
327
+ }
328
+
329
+ [data-ui='switch'][data-part='root'][data-label-placement='start'] > [data-part='label'],
330
+ [data-ui='switch'][data-part='root'][data-label-placement='start'] > [data-part='description'] {
331
+ grid-column: 1;
332
+ }
333
+
334
+ /* Before the pill, the label takes the row, so the row is the settings row. */
335
+ [data-ui='switch'][data-part='root'][data-label-placement='start'] > [data-part='label'] {
336
+ justify-self: stretch;
337
+ }
338
+
339
+ /* The control reaches across the gap towards the label, now on its start side. */
340
+ [data-ui='switch'][data-part='root'][data-label-placement='start']:has(> [data-part='label'])
341
+ > [data-part='control'] {
342
+ margin-inline-end: 0;
343
+ margin-inline-start: calc(-1 * var(--_gap));
344
+ }
345
+
346
+ /* ── Size ─────────────────────────────────────────────────────────────
347
+ *
348
+ * Three steps, matching the field the control sits in. The control reads the
349
+ * field's step before its own default, because a `size="sm"` field holding
350
+ * an `md` control is a field and a control disagreeing about one row.
351
+ */
352
+
353
+ [data-ui='switch'][data-part='root'][data-size='sm'] {
354
+ --_thumb-size: var(--slotted-control-icon-size-sm, 14px);
355
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
356
+ --_gap: var(--slotted-control-label-gap-sm, 8px);
357
+ }
358
+
359
+ [data-ui='switch'][data-part='root'][data-size='md'] {
360
+ --_thumb-size: var(--slotted-control-icon-size-md, 16px);
361
+ --_font-size: var(--slotted-control-font-size-md, 13px);
362
+ --_gap: var(--slotted-control-label-gap-md, 12px);
363
+ }
364
+
365
+ [data-ui='switch'][data-part='root'][data-size='lg'] {
366
+ --_thumb-size: var(--slotted-control-icon-size-lg, 18px);
367
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
368
+ --_gap: var(--slotted-control-label-gap-lg, 12px);
369
+ }
370
+
371
+ /* ── States ───────────────────────────────────────────────────────────
372
+ *
373
+ * Written strongest last, and each weaker one guarded against every stronger
374
+ * one it can meet, which is the order `STATE_PRECEDENCE` sets.
375
+ *
376
+ * Each state is matched on the element as well as on the attribute the
377
+ * binding mirrors. The element is the truth a mirror can lag behind: a
378
+ * control inside a disabled `fieldset` is never told that it is disabled,
379
+ * and the deleted `input` family shipped that defect once already.
380
+ */
381
+
382
+ [data-ui='switch'][data-part='root']:is([data-checked], :has(> [data-part='control']:checked))
383
+ > [data-part='track'] {
384
+ background-color: var(
385
+ --slotted-switch-checked-background,
386
+ var(--slotted-tone-accent-solid, Highlight)
387
+ );
388
+ border-color: var(
389
+ --slotted-switch-checked-border-color,
390
+ var(--slotted-tone-accent-solid, Highlight)
391
+ );
392
+ }
393
+
394
+ [data-ui='switch'][data-part='root']:is([data-checked], :has(> [data-part='control']:checked))
395
+ > [data-part='track']
396
+ > [data-part='thumb'] {
397
+ border-color: var(
398
+ --slotted-switch-checked-thumb-border-color,
399
+ var(--slotted-tone-accent-solid, Highlight)
400
+ );
401
+ inset-inline-start: calc(var(--_gutter) + var(--_travel));
402
+ }
403
+
404
+ [data-ui='switch'][data-part='root']:is(
405
+ [data-checked],
406
+ :has(> [data-part='control']:checked)
407
+ ):not([data-loading])
408
+ > [data-part='track']
409
+ > [data-part='thumb']
410
+ > [data-ui='icon'] {
411
+ opacity: 1;
412
+ }
413
+
414
+ /**
415
+ * Hover and press are the track's, because the thumb is the part that says
416
+ * where the value is and a second thing moving under the pointer only
417
+ * competes with it. Both are held back for disabled, loading and read-only,
418
+ * and both are written before `invalid`, which outranks them.
419
+ */
420
+ [data-ui='switch'][data-part='root']:hover:not([data-disabled]):not([data-loading]):not(
421
+ [data-readonly]
422
+ ):not(:has(> [data-part='control']:disabled))
423
+ > [data-part='track'] {
424
+ background-color: var(
425
+ --slotted-switch-hover-background,
426
+ var(--slotted-tone-secondary-solid-hover, ButtonFace)
427
+ );
428
+ }
429
+
430
+ [data-ui='switch'][data-part='root']:is(
431
+ [data-checked],
432
+ :has(> [data-part='control']:checked)
433
+ ):hover:not([data-disabled]):not([data-loading]):not([data-readonly]):not(
434
+ :has(> [data-part='control']:disabled)
435
+ )
436
+ > [data-part='track'] {
437
+ background-color: var(
438
+ --slotted-switch-checked-hover-background,
439
+ var(--slotted-tone-accent-solid-hover, Highlight)
440
+ );
441
+ border-color: var(
442
+ --slotted-switch-checked-hover-background,
443
+ var(--slotted-tone-accent-solid-hover, Highlight)
444
+ );
445
+ }
446
+
447
+ [data-ui='switch'][data-part='root']:active:not([data-disabled]):not([data-loading]):not(
448
+ [data-readonly]
449
+ ):not(:has(> [data-part='control']:disabled))
450
+ > [data-part='track'] {
451
+ background-color: var(
452
+ --slotted-switch-active-background,
453
+ var(--slotted-tone-secondary-solid-active, ButtonFace)
454
+ );
455
+ }
456
+
457
+ [data-ui='switch'][data-part='root']:is(
458
+ [data-checked],
459
+ :has(> [data-part='control']:checked)
460
+ ):active:not([data-disabled]):not([data-loading]):not([data-readonly]):not(
461
+ :has(> [data-part='control']:disabled)
462
+ )
463
+ > [data-part='track'] {
464
+ background-color: var(
465
+ --slotted-switch-checked-active-background,
466
+ var(--slotted-tone-accent-solid-active, Highlight)
467
+ );
468
+ border-color: var(
469
+ --slotted-switch-checked-active-background,
470
+ var(--slotted-tone-accent-solid-active, Highlight)
471
+ );
472
+ }
473
+
474
+ /**
475
+ * The ring belongs to the track, because the track is the switch a reader
476
+ * sees; the control it answers is the transparent element over it. The
477
+ * offset keeps the ring off the pill's own edge, which is what makes a
478
+ * focused unchecked switch legible against a sunken surface.
479
+ */
480
+ [data-ui='switch'][data-part='root']:has(> [data-part='control']:focus-visible)
481
+ > [data-part='track'] {
482
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
483
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
484
+ }
485
+
486
+ /**
487
+ * Read-only stays focusable, announced and submitted, and it must not be
488
+ * mistakable for disabled. The pill goes hollow rather than grey: the state
489
+ * keeps its own colour in the mark, and the ground stops saying "press me".
490
+ */
491
+ [data-ui='switch'][data-part='root'][data-readonly] > [data-part='track'] {
492
+ background-color: var(--slotted-switch-readonly-background, var(--slotted-surface, Canvas));
493
+ }
494
+
495
+ [data-ui='switch'][data-part='root'][data-readonly] > [data-part='control'],
496
+ [data-ui='switch'][data-part='root'][data-readonly] > [data-part='label'] {
497
+ cursor: default;
498
+ }
499
+
500
+ /**
501
+ * Read-only outranks checked, so it is written with enough weight to take
502
+ * the ground back from it: the pill is hollow whichever state it holds.
503
+ *
504
+ * The edge stays the neutral control edge rather than turning accent. A
505
+ * focused read-only switch drew the ring and the border in the same blue,
506
+ * two pixels apart, and neither could be told from the other. The state is
507
+ * carried where it always is — the thumb's position, and the mark on it.
508
+ */
509
+ [data-ui='switch'][data-part='root'][data-readonly]:is(
510
+ [data-checked],
511
+ :has(> [data-part='control']:checked)
512
+ )
513
+ > [data-part='track'] {
514
+ background-color: var(--slotted-switch-readonly-background, var(--slotted-surface, Canvas));
515
+ border-color: var(
516
+ --slotted-switch-readonly-border-color,
517
+ var(--slotted-control-border-color, ButtonBorder)
518
+ );
519
+ }
520
+
521
+ [data-ui='switch'][data-part='root'][data-readonly]:is(
522
+ [data-checked],
523
+ :has(> [data-part='control']:checked)
524
+ )
525
+ > [data-part='track']
526
+ > [data-part='thumb'] {
527
+ border-color: var(
528
+ --slotted-switch-readonly-border-color,
529
+ var(--slotted-control-border-color, ButtonBorder)
530
+ );
531
+ }
532
+
533
+ /**
534
+ * Invalid outranks hover and press and is written after them: a setting
535
+ * reporting an error should not stop looking wrong because the pointer is
536
+ * over it.
537
+ *
538
+ * The ring is retargeted rather than repainted. Every rule that draws focus
539
+ * reads `--slotted-focus-ring-color`, so pointing that one name at the
540
+ * invalid colour on the root moves the ring without restating it — the rule
541
+ * `text-field` wrote for this family to inherit.
542
+ */
543
+ [data-ui='switch'][data-part='root'][data-invalid] {
544
+ --slotted-focus-ring-color: var(
545
+ --slotted-switch-invalid-focus-ring-color,
546
+ var(--slotted-focus-ring-color-invalid, Highlight)
547
+ );
548
+ }
549
+
550
+ [data-ui='switch'][data-part='root'][data-invalid] > [data-part='track'],
551
+ [data-ui='switch'][data-part='root'][data-invalid] > [data-part='track'] > [data-part='thumb'] {
552
+ border-color: var(--slotted-switch-invalid-border-color, var(--slotted-tone-danger-border));
553
+ /*
554
+ * Width as well as colour (WCAG 1.4.1), the edge `text-field` draws: in
555
+ * `high-contrast` the danger border is the resting edge's black. An edge
556
+ * inside the border of the track and of the thumb, so neither moves, in
557
+ * place of the thumb's resting lift; the thumb carries it where a checked
558
+ * track's fill would hide the track's own.
559
+ */
560
+ --_invalid-edge-width: var(
561
+ --slotted-switch-invalid-edge-width,
562
+ var(--slotted-control-invalid-edge-width, 1px)
563
+ );
564
+
565
+ box-shadow: inset 0 0 0 var(--_invalid-edge-width)
566
+ var(--slotted-switch-invalid-border-color, var(--slotted-tone-danger-border));
567
+ }
568
+
569
+ /**
570
+ * Loading. The geometry does not change — the thumb stays where it is and
571
+ * the track keeps its size — because a control that resizes while it saves
572
+ * moves the row under the pointer. The mark gives way to the arc, which is
573
+ * the one thing in the family that says the value is not settled yet.
574
+ */
575
+ [data-ui='switch'][data-part='root'][data-loading] > [data-part='control'] {
576
+ cursor: progress;
577
+ }
578
+
579
+ [data-ui='switch'][data-part='root'][data-loading]
580
+ > [data-part='track']
581
+ > [data-part='thumb']
582
+ > [data-ui='icon'] {
583
+ opacity: 0;
584
+ }
585
+
586
+ /**
587
+ * The arc. It is geometry rather than an icon — a ring with one quarter of
588
+ * its border left transparent — so no glyph token is invented for a shape
589
+ * the border already draws. `progress-indicator` is wave 2 and not built;
590
+ * when it is, this is the first thing it should replace.
591
+ *
592
+ * Three quarters rather than one. A single coloured side read as a shape
593
+ * rather than as a spinner in a still image, and a still image is what a
594
+ * reader under `prefers-reduced-motion` is looking at.
595
+ */
596
+ [data-ui='switch'][data-part='root'][data-loading]
597
+ > [data-part='track']
598
+ > [data-part='thumb']::after {
599
+ animation: slotted-switch-pending var(--slotted-duration-loop) linear infinite;
600
+ border: var(--slotted-border-width-thin, 1px) solid
601
+ var(--slotted-switch-pending-color, var(--slotted-tone-accent-solid));
602
+ border-inline-start-color: transparent;
603
+ border-radius: var(--slotted-radius-full, 9999px);
604
+ box-sizing: border-box;
605
+ content: '';
606
+ display: block;
607
+ /* Inset by an eighth so the arc sits inside the thumb's own edge. */
608
+ inset: 12.5%;
609
+ position: absolute;
610
+ }
611
+
612
+ @keyframes slotted-switch-pending {
613
+ to {
614
+ rotate: 360deg;
615
+ }
616
+ }
617
+
618
+ /**
619
+ * An animation that repeats forever has to stop rather than hurry, which is
620
+ * what `docs/token-foundation.md` requires and what the motion scale cannot
621
+ * do for a loop. The arc stays drawn, so the pending state is still visible
622
+ * without anything moving.
623
+ */
624
+ @media (prefers-reduced-motion: reduce) {
625
+ [data-ui='switch'][data-part='root'][data-loading]
626
+ > [data-part='track']
627
+ > [data-part='thumb']::after {
628
+ animation: none;
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Disabled is the strongest state and is written last. `:disabled` on the
634
+ * control is matched beside `[data-disabled]` on the root, because a control
635
+ * inside a disabled `fieldset` is disabled by something that never told it
636
+ * so — and left to the attribute alone it would look fully operable and
637
+ * answer nothing.
638
+ */
639
+ [data-ui='switch'][data-part='root']:is([data-disabled], :has(> [data-part='control']:disabled))
640
+ > [data-part='control'],
641
+ [data-ui='switch'][data-part='root']:is([data-disabled], :has(> [data-part='control']:disabled))
642
+ > [data-part='label'] {
643
+ cursor: not-allowed;
644
+ }
645
+
646
+ [data-ui='switch'][data-part='root']:is([data-disabled], :has(> [data-part='control']:disabled))
647
+ > [data-part='track'] {
648
+ background-color: var(
649
+ --slotted-switch-disabled-background,
650
+ var(--slotted-disabled-background, ButtonFace)
651
+ );
652
+ border-color: var(--slotted-switch-disabled-border-color, var(--slotted-disabled-border));
653
+ }
654
+
655
+ /**
656
+ * Muted to the colour the theme measures rather than faded by opacity: a
657
+ * setting nobody can read is a setting nobody can check, and a disabled form
658
+ * still has to say what it holds.
659
+ */
660
+ [data-ui='switch'][data-part='root']:is([data-disabled], :has(> [data-part='control']:disabled))
661
+ > [data-part='track']
662
+ > [data-part='thumb'] {
663
+ /**
664
+ * Grey rather than white. The knob is the brightest thing in the family,
665
+ * and a bright knob on a grey track reads as a control still waiting to be
666
+ * used. The mark inverts with it, so the state stays readable.
667
+ */
668
+ background-color: var(
669
+ --slotted-switch-disabled-thumb-background,
670
+ var(--slotted-disabled-foreground)
671
+ );
672
+ border-color: var(--slotted-switch-disabled-border-color, var(--slotted-disabled-border));
673
+ box-shadow: none;
674
+ }
675
+
676
+ [data-ui='switch'][data-part='root']:is([data-disabled], :has(> [data-part='control']:disabled))
677
+ > [data-part='track']
678
+ > [data-part='thumb']
679
+ > [data-ui='icon'] {
680
+ color: var(--slotted-switch-disabled-mark-color, var(--slotted-disabled-background));
681
+ }
682
+
683
+ [data-ui='switch'][data-part='root']:is([data-disabled], :has(> [data-part='control']:disabled))
684
+ > [data-part='label'],
685
+ [data-ui='switch'][data-part='root']:is([data-disabled], :has(> [data-part='control']:disabled))
686
+ > [data-part='description'] {
687
+ color: var(--slotted-switch-disabled-color, var(--slotted-disabled-foreground, GrayText));
688
+ }
689
+
690
+ /*
691
+ * Forced colours remove every box shadow, and the edge with them. The width
692
+ * comes back as an outline inside the border, on the
693
+ * track and on the thumb; the focus ring takes the track and the thumb keeps
694
+ * its edge.
695
+ */
696
+ @media (forced-colors: active) {
697
+ [data-ui='switch'][data-part='root'][data-invalid] > [data-part='track'],
698
+ [data-ui='switch'][data-part='root'][data-invalid] > [data-part='track'] > [data-part='thumb'] {
699
+ outline: var(--_invalid-edge-width) solid CanvasText;
700
+ outline-offset: calc(-1 * (var(--_border) + var(--_invalid-edge-width)));
701
+ }
702
+ }
703
+ }