@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,1593 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── A control that shows one choice and opens a list of the rest ────────
4
+ *
5
+ * The family is two subtrees, and that is the first thing to know about this
6
+ * sheet. The **control** sits where the consumer wrote it, inside a field,
7
+ * inside a form. The **popup** is portalled to the end of the document so it
8
+ * escapes every clipping ancestor between the two. Nothing selects one from
9
+ * the other, so `data-ui='select'` is written on both roots — the contract
10
+ * records it under `portalledIdentity` — and every rule below states which of
11
+ * the two it is about through its own `data-part`.
12
+ *
13
+ * ┌─ root ─ the control ───────────────────────────────────────┐
14
+ * │ ┌─ trigger ─ role="combobox" ─ fills the frame ─────────┐ │
15
+ * │ │ value ▸ "Blueberry" │ │
16
+ * │ └───────────────────────────────────────────────────────┘ │
17
+ * │ clear ✕ arrow ⌄ │
18
+ * └────────────────────────────────────────────────────────────┘
19
+ *
20
+ * ┌─ positioner ─ the layer ───────────────────────────────────┐
21
+ * │ ┌─ content ─ role="listbox" ─ scrolls ───────────────┐ │
22
+ * │ │ group ▸ group-label │ │
23
+ * │ │ item ▸ icon │ label │ indicator │ │
24
+ * │ │ description │ │
25
+ * │ │ separator · empty · status │ │
26
+ * │ └────────────────────────────────────────────────────┘ │
27
+ * └────────────────────────────────────────────────────────────┘
28
+ *
29
+ * Under `search` the popup gains one row and the list moves one element in,
30
+ * because a text field is not an option and a listbox holds nothing else:
31
+ *
32
+ * ┌─ content ─ role="dialog" ─ data-search ────────────┐
33
+ * │ search ▸ search-icon │ search-input │
34
+ * │ ┌─ list ─ role="listbox" ─ scrolls ───────────┐ │
35
+ * │ │ item ▸ icon │ label ▸ match │ indicator │ │
36
+ * │ └──────────────────────────────────────────────┘ │
37
+ * └────────────────────────────────────────────────────┘
38
+ *
39
+ * Every part above `list` exists only under `search`, so a consumer who does
40
+ * not ask for it renders the DOM they rendered before it existed.
41
+ *
42
+ * ── Why the frame is `text-field`'s frame ────────────────────────────
43
+ *
44
+ * The border, the radius, the resting shadow, the focus ring, the invalid
45
+ * retargeting and the disabled treatment are the same declarations reading
46
+ * the same tokens. A form holding a text field, a number field and a select
47
+ * is one row of controls or it is three libraries, and none of the eight
48
+ * implementations surveyed for this family has an opinion here because none
49
+ * of them ships the other two.
50
+ *
51
+ * The trigger is a borderless button filling that frame, so the frame draws
52
+ * the ring for it through `:has()` — the rule `text-field` already writes for
53
+ * its input — and `clear` is a sibling of the trigger rather than a child of
54
+ * it, because a focusable descendant of a `combobox` is a widget inside a
55
+ * widget. The chevron and the clear button then sit on top of the trigger
56
+ * rather than beside it, for the reason written above the trigger's own rule.
57
+ *
58
+ * ── Why the popup is a grid and the item is a subgrid ────────────────
59
+ *
60
+ * The same architecture `menu` uses, and for the same reason: the surface
61
+ * owns three columns — the icon, the label, the check — and every item,
62
+ * group and group label passes through them with `grid-template-columns:
63
+ * subgrid`, so labels line up across the whole list however many of the
64
+ * options carry artwork.
65
+ *
66
+ * The check sits in the **last** column rather than sharing the first with
67
+ * the icon, which is where this family parts company with `menu`. A menu's
68
+ * check *is* its item's icon — an option's is not: a country's flag and the
69
+ * mark saying it is chosen are two different facts about one row, and a
70
+ * reader who can see only one of them at a time cannot scan the list. The
71
+ * columns carry no `column-gap`, so a list with neither artwork nor a check
72
+ * collapses both gutters to nothing.
73
+ */
74
+
75
+ /* ── The control ──────────────────────────────────────────────────────── */
76
+
77
+ [data-ui='select'][data-part='root'] {
78
+ /**
79
+ * Type, height, inline padding, gap and glyph size come from the control
80
+ * scales — the only ones the theme restates per density — so a select in a
81
+ * dense table shrinks whole rather than in parts.
82
+ */
83
+ --_font-size: var(--slotted-control-font-size-md, 13px);
84
+ --_height: var(--slotted-control-height-md, 34px);
85
+ --_padding-inline: var(--slotted-control-padding-inline-md, 13px);
86
+ --_gap: var(--slotted-control-gap-md, 8px);
87
+ /** The floor under the glyph scale at the dense step (SL3); a family token so a theme can retune it. */
88
+ --_icon-floor: var(--slotted-select-icon-size-min, 14px);
89
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
90
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
91
+
92
+ /**
93
+ * The drawn clear button, sized by its glyph and capped at the inner size
94
+ * of a control at this step (`--slotted-control-inner-size-*`). The
95
+ * arithmetic is `text-field`'s, because the two buttons are the same button
96
+ * in the same frame and a second answer would drift from the first.
97
+ */
98
+ --_action-size: var(
99
+ --slotted-select-action-size,
100
+ min(calc(var(--_icon-size) + 2 * var(--slotted-space-2, 0.25rem)), var(--_inner-size))
101
+ );
102
+
103
+ align-items: center;
104
+ background-color: var(--slotted-select-background, var(--slotted-control-surface, Field));
105
+ border: var(--slotted-control-border-width, 1px) solid
106
+ var(--slotted-select-border-color, var(--slotted-control-border-color, ButtonBorder));
107
+ border-radius: var(--slotted-select-radius, var(--slotted-control-radius, 4px));
108
+ box-shadow: var(--slotted-select-shadow, var(--slotted-control-shadow, none));
109
+ box-sizing: border-box;
110
+ color: var(--slotted-select-color, var(--slotted-text, CanvasText));
111
+ column-gap: var(--_gap);
112
+ /**
113
+ * Two lanes on one row: what the control holds, and what ends it (SL9).
114
+ *
115
+ * The chips used to be a row hanging over the trigger's content box, with
116
+ * its inline end written as the same arithmetic the chevron and the clear
117
+ * button are placed by. A row that hangs reserves no space, so the lane it
118
+ * may use was a number kept in step by hand, and six chosen values ran
119
+ * under the two controls that end the control. Here the lane *is* a track:
120
+ * it ends where the next track begins, and no arithmetic can be out of
121
+ * date. The trigger spans both, so it is still the ground every press that
122
+ * is not a chip or the clear button lands on.
123
+ */
124
+ display: grid;
125
+ font-family: var(--slotted-control-font-family, system-ui, sans-serif);
126
+ font-size: var(--_font-size);
127
+ grid-template-columns: minmax(0, 1fr) auto;
128
+ inline-size: 100%;
129
+ /* A control in a grid track narrower than its intrinsic width would
130
+ otherwise refuse to shrink and take the row's edge with it. */
131
+ min-inline-size: 0;
132
+ /* A minimum rather than a height: the frame is the taller of the control
133
+ scale and whatever a consumer put in the value. */
134
+ min-block-size: var(--_height);
135
+ /* The containing block of the chevron and the clear button, both of which
136
+ are placed against the frame's inline end rather than laid out in it. */
137
+ position: relative;
138
+ transition:
139
+ background-color var(--slotted-control-transition-duration)
140
+ var(--slotted-control-transition-easing),
141
+ border-color var(--slotted-control-transition-duration)
142
+ var(--slotted-control-transition-easing),
143
+ box-shadow var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
144
+ }
145
+
146
+ /* And the clear button's own lane, before the chevron's, when there is one. */
147
+ [data-ui='select'][data-part='root']:has(> [data-part='clear']) {
148
+ grid-template-columns: minmax(0, 1fr) auto auto;
149
+ }
150
+
151
+ /**
152
+ * A select that does not fill its field. The frame shrinks to its widest
153
+ * value, which is what a filter beside a table needs and what a field never
154
+ * does — hence the default the contract states, `fullWidth: true`.
155
+ */
156
+ [data-ui='select'][data-part='root']:not([data-full-width]) {
157
+ inline-size: auto;
158
+ }
159
+
160
+ /**
161
+ * The control. It gives up its own frame entirely — border, ground, ring and
162
+ * radius all belong to the box around it — and keeps what makes it a button:
163
+ * the press, the `Enter` and `Space` the platform already handles, and every
164
+ * native attribute the consumer wrote.
165
+ *
166
+ * **It fills the frame, and the two marks sit on top of it.** The chevron
167
+ * belongs at the very end of the control — it is the affordance for the whole
168
+ * thing, which is where a native `select` draws it and where every surveyed
169
+ * implementation puts it — and the clear button belongs before it. In document
170
+ * order the clear button is a sibling of the trigger, so a chevron inside the
171
+ * trigger can only ever be drawn *before* the clear button, and the first
172
+ * capture of this family showed exactly that: `Blueberry ⌄ ✕`.
173
+ *
174
+ * So the chevron and the clear button stand in their own lane at the frame's
175
+ * inline end, and the trigger spans every lane and reserves their width as
176
+ * its own end padding. Padding is part of the trigger's box, so a press on
177
+ * the chevron still lands on the button that opens the popup — which is why
178
+ * the chevron can be inert and the arrangement costs nothing.
179
+ */
180
+ [data-ui='select'] > [data-part='trigger'] {
181
+ align-items: center;
182
+ appearance: none;
183
+ background-color: transparent;
184
+ border: 0;
185
+ box-sizing: border-box;
186
+ color: inherit;
187
+ /**
188
+ * The hand, as `button/button.css` draws it on every button in the
189
+ * library. This drew an arrow until SL11, and the arrow came from the
190
+ * native `select` — the element this family refused, on the grounds that
191
+ * the operating system draws its popup and a web surface is not what it
192
+ * gets. Keeping the native cursor after refusing the native element left
193
+ * the one control in a form that says nothing happens when it is pressed.
194
+ */
195
+ cursor: pointer;
196
+ display: flex;
197
+ font: inherit;
198
+ gap: var(--_gap);
199
+ /* The ground: every lane on the one row, so nothing the reader presses
200
+ between the chips and the border falls past the button. */
201
+ grid-area: 1 / 1 / 2 / -1;
202
+ /* The frame's border box, not its content box: on a coarse pointer the
203
+ frame is the touch target's height and the trigger is too. */
204
+ margin-block: calc(var(--slotted-control-border-width, 1px) * -1);
205
+ min-block-size: var(--_height);
206
+ /* An `auto` minimum refuses to shrink below the longest label, which is how
207
+ a select in a narrow column pushes its own field off the row. */
208
+ min-inline-size: 0;
209
+ /* The frame draws the ring for it, so two rings never nest. */
210
+ outline: none;
211
+ padding-block: 0;
212
+ padding-inline-start: var(--_padding-inline);
213
+ /* The inset, the chevron and the gap before it. */
214
+ padding-inline-end: calc(var(--_padding-inline) + var(--_icon-size) + var(--_gap));
215
+ text-align: start;
216
+ touch-action: manipulation;
217
+ -webkit-tap-highlight-color: transparent;
218
+ }
219
+
220
+ /* And the clear button's own box, plus a second gap, when there is one. */
221
+ [data-ui='select'][data-part='root']:has(> [data-part='clear']) > [data-part='trigger'] {
222
+ padding-inline-end: calc(
223
+ var(--_padding-inline) + var(--_icon-size) + var(--_gap) + var(--_action-size) + var(--_gap)
224
+ );
225
+ }
226
+
227
+ /**
228
+ * One line, ending in an ellipsis. A value that wrapped would change the
229
+ * control's height as the reader chose, and a form that reflows on selection
230
+ * is a form that moves under the pointer.
231
+ */
232
+ [data-ui='select'] [data-part='value'] {
233
+ flex: 1 1 auto;
234
+ min-inline-size: 0;
235
+ overflow: hidden;
236
+ text-overflow: ellipsis;
237
+ white-space: nowrap;
238
+ }
239
+
240
+ /**
241
+ * Nothing chosen yet. The same weight the reader will see once they choose,
242
+ * one step quieter, so the control does not change shape when it fills.
243
+ */
244
+ [data-ui='select'] [data-part='value'][data-placeholder] {
245
+ color: var(--slotted-select-placeholder-color, var(--slotted-text-muted, GrayText));
246
+ }
247
+
248
+ /**
249
+ * A value the collection could not resolve to a label, which is what an
250
+ * asynchronous list holds for one render. It is not the placeholder — the
251
+ * control has a value — and it is not a normal label either, so it is set in
252
+ * the same face and marked as unresolved for a stylesheet that wants to say
253
+ * more.
254
+ */
255
+ [data-ui='select'] [data-part='value'][data-unmatched] {
256
+ font-variant-numeric: tabular-nums;
257
+ }
258
+
259
+ /* ── Several chosen values, as the chips they are ────────────────────── */
260
+
261
+ /**
262
+ * The preview of a multiple selection (SL4). It was a sentence about the
263
+ * choice — "Apple +1" — and it is now the choice: one `tag` per value, each
264
+ * with the remove control a tag already has, and one more chip counting what
265
+ * did not fit.
266
+ *
267
+ * The chips are `tag`'s, worn as `data-ui` beside this row, so the chip a
268
+ * reader removes here is the chip they know from everywhere else and its
269
+ * geometry is stated once. Nothing about a chip is decided here: the row
270
+ * asks for the small step and the chip answers with its own arithmetic,
271
+ * floored at the target minimum because its remove control is a control.
272
+ *
273
+ * **The row is a sibling of the trigger, and a lane of the control** (SL9).
274
+ * The trigger is a `button`, and a button cannot contain a remove control:
275
+ * that is a control inside a control, which no browser and no reader resolves
276
+ * the same way. So the row is written beside the button rather than in it —
277
+ * and it takes the first track of the frame's row, so the space it needs is
278
+ * space the control gives it and the chips end where the lane of the final
279
+ * controls begins. It lets every press it does not want through to the
280
+ * trigger spanning underneath, so the frame still opens the popup everywhere
281
+ * a chip is not.
282
+ */
283
+ [data-ui='select'][data-part='root'] > [data-part='tags'] {
284
+ align-items: center;
285
+ display: flex;
286
+ gap: var(--slotted-space-2, 0.25rem);
287
+ /* The content lane: the chips share it with the trigger's own text. */
288
+ grid-area: 1 / 1 / 2 / 2;
289
+ /* The gutter the value begins from, so a chip starts where a label would. */
290
+ margin-inline-start: var(--_padding-inline);
291
+ /**
292
+ * The row is at least a target tall, because a chip's remove control takes
293
+ * the rest of its own target from a transparent halo that stands outside
294
+ * the chip (TG2), and the clip below would otherwise cut that halo off at
295
+ * the chip's edge — the press area measured 24 × 22.8 inside the trigger
296
+ * and 24 × 24 everywhere else. The chips stay centred in it. It is never
297
+ * taller than the control's own inner box, so the height of the frame is
298
+ * the height of a select that chose one thing.
299
+ */
300
+ min-block-size: var(--slotted-target-min, 24px);
301
+ /* A lane a chip cannot widen: the track is `minmax(0, 1fr)` and this is
302
+ what makes a chip shrink against it rather than push it. */
303
+ min-inline-size: 0;
304
+ /* A chip that does not fit is hidden by the collapse, not clipped; this is
305
+ the guard for the frame between one measurement and the next. */
306
+ overflow: hidden;
307
+ /* The gaps between chips belong to the trigger underneath. */
308
+ pointer-events: none;
309
+ }
310
+
311
+ [data-ui='select'] > [data-part='tags'] > * {
312
+ /* Each chip takes its own presses back, and stands above the trigger that
313
+ spans the lane underneath it. */
314
+ pointer-events: auto;
315
+ position: relative;
316
+ }
317
+
318
+ /**
319
+ * The chip that counts what did not fit. It is a chip and not a number
320
+ * beside them, so the row reads as one run of chips whose last one happens
321
+ * to be a count, and `createCollapse` can measure it the way it measures
322
+ * every other unit.
323
+ */
324
+ [data-ui='select'] > [data-part='tags'] > [data-overflow] {
325
+ flex: none;
326
+ }
327
+
328
+ /**
329
+ * The sentence, for a control too narrow for a chip. It lives inside the
330
+ * value, so the trigger is named by it whichever of the two is drawn, and it
331
+ * is taken out of sight rather than out of the tree while the chips are
332
+ * shown: a button whose accessible name disappeared when the frame grew is a
333
+ * button that changes what it is called when the window is resized.
334
+ */
335
+ [data-ui='select'] [data-part='summary'] {
336
+ display: block;
337
+ overflow: hidden;
338
+ text-overflow: ellipsis;
339
+ /* Its own bidi run, ordered by its own content, so a Latin count in a
340
+ right-to-left row is clipped at its own end rather than at the page's —
341
+ the repair `status` and `empty` already make, and for the same reason. */
342
+ unicode-bidi: plaintext;
343
+ white-space: nowrap;
344
+ }
345
+
346
+ [data-ui='select'][data-part='root']:not([data-narrow]) [data-part='summary'] {
347
+ block-size: 1px;
348
+ clip-path: inset(50%);
349
+ inline-size: 1px;
350
+ overflow: hidden;
351
+ white-space: nowrap;
352
+ }
353
+
354
+ [data-ui='select'][data-part='root'][data-narrow] > [data-part='tags'] {
355
+ display: none;
356
+ }
357
+
358
+ /**
359
+ * The chevron. The library's own `Glyph` — an `<svg>` the binding draws
360
+ * inside an `icon` box — rather than a character, and the box turns over when
361
+ * the popup opens: the one movement in the control, and the only thing on the
362
+ * closed control that says it opens at all.
363
+ */
364
+ [data-ui='select'] > [data-part='arrow'] {
365
+ --slotted-icon-size: var(--_icon-size);
366
+
367
+ align-items: center;
368
+ block-size: var(--_icon-size);
369
+ color: var(--slotted-select-arrow-color, var(--slotted-text-muted, GrayText));
370
+ display: flex;
371
+ /* The last lane, whichever of the two arrangements is in force. */
372
+ grid-area: 1 / -2 / 2 / -1;
373
+ inline-size: var(--_icon-size);
374
+ justify-content: center;
375
+ margin-inline-end: var(--_padding-inline);
376
+ /* Inert on purpose: the trigger spans this lane underneath it, so a press
377
+ here opens the popup rather than landing on a decorative span. */
378
+ pointer-events: none;
379
+ }
380
+
381
+ [data-ui='select'] > [data-part='arrow'] > [data-ui='icon'] {
382
+ transition: rotate var(--slotted-control-transition-duration)
383
+ var(--slotted-control-transition-easing);
384
+ }
385
+
386
+ [data-ui='select'][data-open] > [data-part='arrow'] > [data-ui='icon'] {
387
+ rotate: 180deg;
388
+ }
389
+
390
+ /**
391
+ * The ring, where the chevron stands. While an answer is travelling the
392
+ * control shows the ring instead of the chevron: the chevron says the popup
393
+ * opens, and it does not open onto anything yet (SL6).
394
+ *
395
+ * The arc is not drawn here. It is the one `spinner/spinner.css` draws for
396
+ * every family that waits — B4 extracted it for exactly this second host —
397
+ * and the element wears `data-ui='spinner'` beside this family's own part.
398
+ * All this rule does is stand it where the chevron stood and give it the
399
+ * chevron's size and ink, through the size token the shared sheet reads.
400
+ */
401
+ [data-ui='select'] > [data-part='busy'] {
402
+ --slotted-spinner-size: var(--_icon-size);
403
+
404
+ color: var(--slotted-select-arrow-color, var(--slotted-text-muted, GrayText));
405
+ /* The chevron's lane, because it stands where the chevron stood. */
406
+ grid-area: 1 / -2 / 2 / -1;
407
+ margin-inline-end: var(--_padding-inline);
408
+ /* Inert on purpose, as the chevron is: the trigger spans this lane
409
+ underneath it, so a press here opens the popup. */
410
+ pointer-events: none;
411
+ }
412
+
413
+ /**
414
+ * The clear button, in the lane before the chevron's. Its box is wider than
415
+ * its glyph by the target it recovers, and that overhang is taken back on
416
+ * both sides — so every gap in the lane is measured between glyphs and the
417
+ * *glyph* keeps the rhythm the value begins with. `text-field`'s arithmetic,
418
+ * stated in the terms a track states it.
419
+ */
420
+ [data-ui='select'] > [data-part='clear'] {
421
+ align-items: center;
422
+ appearance: none;
423
+ background-color: transparent;
424
+ block-size: var(--_action-size);
425
+ border: 0;
426
+ border-radius: var(--slotted-select-radius, var(--slotted-control-radius, 4px));
427
+ color: var(--slotted-select-action-color, var(--slotted-text-muted, GrayText));
428
+ cursor: pointer;
429
+ display: inline-flex;
430
+ grid-area: 1 / 2 / 2 / 3;
431
+ inline-size: var(--_action-size);
432
+ justify-content: center;
433
+ margin-inline: calc((var(--_icon-size) - var(--_action-size)) / 2);
434
+ /* The user agent pads a button, and inside a box sized to its glyph that
435
+ padding squeezed the glyph to twelve pixels. */
436
+ padding: 0;
437
+ /* Over the trigger that spans this lane, so it takes its own presses back
438
+ from the button running underneath it. */
439
+ position: relative;
440
+ touch-action: manipulation;
441
+ transition:
442
+ background-color var(--slotted-control-transition-duration)
443
+ var(--slotted-control-transition-easing),
444
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
445
+ -webkit-tap-highlight-color: transparent;
446
+ }
447
+
448
+ /**
449
+ * The hit area, recovered outside the paint. At `sm` with `dense` the whole
450
+ * control is 24 pixels tall — the WCAG 2.2 minimum — so nothing drawn inside
451
+ * it can also be 24. This grows the target back in both axes and leaves a
452
+ * larger one alone.
453
+ */
454
+ [data-ui='select'] > [data-part='clear']::after {
455
+ content: '';
456
+ inset: min(0px, calc((100% - var(--slotted-target-min, 24px)) / 2));
457
+ position: absolute;
458
+ }
459
+
460
+ /* On a finger the target is the comfortable size in both axes and the box
461
+ is the one a mouse sees: the ground and the focus ring stay square, and
462
+ the hit area above grows to 44 pixels around them. It does not reach into
463
+ the chevron's lane after it, which is the trigger's to answer. */
464
+ @media (pointer: coarse) {
465
+ [data-ui='select'] > [data-part='clear']::after {
466
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
467
+ inset-inline-end: 0;
468
+ }
469
+ }
470
+
471
+ /* The cross is a `Glyph` the binding draws, at the chevron's size. */
472
+ [data-ui='select'] > [data-part='clear'] {
473
+ --slotted-icon-size: var(--_icon-size);
474
+ }
475
+
476
+ /* ── Size ─────────────────────────────────────────────────────────────
477
+ *
478
+ * Three steps, matching the field the control sits in and the popup it
479
+ * opens. The control reads the field's step rather than its own default when
480
+ * there is a field, because a `sm` field holding an `md` control is a field
481
+ * and a control disagreeing about one row.
482
+ */
483
+
484
+ [data-ui='select'][data-part='root'][data-size='sm'] {
485
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
486
+ --_height: var(--slotted-control-height-sm, 30px);
487
+ --_padding-inline: var(--slotted-control-padding-inline-sm, 10px);
488
+ --_gap: var(--slotted-control-gap-sm, 6px);
489
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-sm, 14px));
490
+ --_inner-size: var(--slotted-control-inner-size-sm, 22px);
491
+ }
492
+
493
+ [data-ui='select'][data-part='root'][data-size='md'] {
494
+ --_font-size: var(--slotted-control-font-size-md, 13px);
495
+ --_height: var(--slotted-control-height-md, 34px);
496
+ --_padding-inline: var(--slotted-control-padding-inline-md, 13px);
497
+ --_gap: var(--slotted-control-gap-md, 8px);
498
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
499
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
500
+ }
501
+
502
+ [data-ui='select'][data-part='root'][data-size='lg'] {
503
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
504
+ --_height: var(--slotted-control-height-lg, 38px);
505
+ --_padding-inline: var(--slotted-control-padding-inline-lg, 16px);
506
+ --_gap: var(--slotted-control-gap-lg, 8px);
507
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-lg, 18px));
508
+ --_inner-size: var(--slotted-control-inner-size-lg, 28px);
509
+ }
510
+
511
+ /* ── Variant ──────────────────────────────────────────────────────────
512
+ *
513
+ * The three `text-field` ships, with the same values, because a form with a
514
+ * `subtle` text field and an `outline` select beside it is a form that looks
515
+ * unfinished. `plain` is what a dense table needs: a cell that is only a
516
+ * control while it is being used, keeping exactly the same box so the row
517
+ * does not reflow when the pointer crosses it.
518
+ */
519
+
520
+ [data-ui='select'][data-part='root'][data-variant='subtle'] {
521
+ background-color: var(
522
+ --slotted-select-subtle-background,
523
+ var(--slotted-surface-sunken, Canvas)
524
+ );
525
+ border-color: var(
526
+ --slotted-select-subtle-border-color,
527
+ var(--slotted-border-subtle, ButtonBorder)
528
+ );
529
+ }
530
+
531
+ /* No edge, no lift, no ground and no corner of its own: the cell it sits in
532
+ is the box, the row is the ground, a cell has no corner, and the resting
533
+ shadow would draw the cell where there is none. */
534
+ [data-ui='select'][data-part='root'][data-variant='plain'] {
535
+ background-color: transparent;
536
+ border-color: transparent;
537
+ border-radius: var(--slotted-radius-none, 0px);
538
+ box-shadow: none;
539
+ }
540
+
541
+ /**
542
+ * The one rule the theme cannot write for the family. A transparent border
543
+ * has no system colour to be remapped to, so the `plain` control would lose
544
+ * its only edge exactly where an edge matters most.
545
+ */
546
+ @media (forced-colors: active) {
547
+ [data-ui='select'][data-part='root'][data-variant='plain'] {
548
+ border-color: ButtonBorder;
549
+ }
550
+ }
551
+
552
+ /* ── States on the control ────────────────────────────────────────────
553
+ *
554
+ * Written strongest last, each weaker one guarded against every stronger one
555
+ * it can meet, which is the order `STATE_PRECEDENCE` sets.
556
+ */
557
+
558
+ [data-ui='select'][data-part='root']:hover:not([data-disabled]):not([data-readonly]):not(
559
+ [data-invalid]
560
+ ) {
561
+ border-color: var(--slotted-select-hover-border-color, var(--slotted-tone-accent-border));
562
+ }
563
+
564
+ [data-ui='select'][data-part='root'][data-variant='subtle']:hover:not([data-disabled]):not(
565
+ [data-readonly]
566
+ ):not([data-invalid]) {
567
+ background-color: var(
568
+ --slotted-select-subtle-hover-background,
569
+ var(--slotted-tone-secondary-subtle-hover)
570
+ );
571
+ }
572
+
573
+ [data-ui='select'][data-part='root'][data-variant='plain']:hover:not([data-disabled]):not(
574
+ [data-readonly]
575
+ ):not([data-invalid]) {
576
+ background-color: var(
577
+ --slotted-select-plain-hover-background,
578
+ var(--slotted-tone-secondary-subtle-hover)
579
+ );
580
+ /* The accent edge the other variants take would draw the cell's outline. */
581
+ border-color: transparent;
582
+ }
583
+
584
+ /**
585
+ * The ring belongs to the frame and answers the trigger's focus. A button
586
+ * matches `:focus-visible` for a keyboard arrival and not for a pointer
587
+ * press, which is exactly right here: a reader who pressed the control can
588
+ * see where they pressed, and the popup is already open under it.
589
+ */
590
+ [data-ui='select'][data-part='root']:has(> [data-part='trigger']:focus-visible) {
591
+ /* The resting shadow, recoloured: the geometry is the foundation's and the
592
+ ink is the ring's, so the lift and the outline are one colour. */
593
+ box-shadow: var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
594
+ var(--slotted-focus-ring-color, Highlight);
595
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
596
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
597
+ }
598
+
599
+ /**
600
+ * The ring of a control whose box is not its own is drawn inside that box,
601
+ * the way the inner controls draw theirs inside the frame: two pixels past
602
+ * the control is two pixels over the next row's rule. No lift either — a
603
+ * shadow that appears only under focus is a cell that changes size when it
604
+ * is used.
605
+ */
606
+ [data-ui='select'][data-part='root'][data-variant='plain']:has(
607
+ > [data-part='trigger']:focus-visible
608
+ ) {
609
+ box-shadow: none;
610
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
611
+ }
612
+
613
+ /**
614
+ * The open control keeps the accent edge whether or not the keyboard opened
615
+ * it. A popup hanging off a control with a resting border reads as two
616
+ * elements that happen to be adjacent.
617
+ */
618
+ [data-ui='select'][data-part='root'][data-open]:not([data-disabled]) {
619
+ border-color: var(--slotted-select-open-border-color, var(--slotted-tone-accent-border));
620
+ }
621
+
622
+ /**
623
+ * Invalid outranks hover and open, and is written after both: a control
624
+ * reporting an error should not stop looking wrong because it was opened.
625
+ * The ring is retargeted rather than repainted — every rule that draws focus
626
+ * reads `--slotted-focus-ring-color`, so pointing that one name at the
627
+ * invalid colour moves the outline and the shadow together.
628
+ */
629
+ [data-ui='select'][data-part='root'][data-invalid] {
630
+ --slotted-focus-ring-color: var(
631
+ --slotted-select-invalid-focus-ring-color,
632
+ var(--slotted-focus-ring-color-invalid, Highlight)
633
+ );
634
+ --_invalid-edge-width: var(
635
+ --slotted-select-invalid-edge-width,
636
+ var(--slotted-control-invalid-edge-width, 1px)
637
+ );
638
+ --_invalid-edge-color: var(
639
+ --slotted-select-invalid-border-color,
640
+ var(--slotted-tone-danger-border)
641
+ );
642
+ --_invalid-edge: inset 0 0 0 var(--_invalid-edge-width) var(--_invalid-edge-color);
643
+
644
+ border-color: var(--slotted-select-invalid-border-color, var(--slotted-tone-danger-border));
645
+ /*
646
+ * Width as well as colour (WCAG 1.4.1), the edge `text-field` draws: in
647
+ * `high-contrast` the danger border is the resting edge's black. The line
648
+ * is a shadow inside the border, so neither box nor text moves, and it
649
+ * takes the resting lift's place.
650
+ */
651
+ box-shadow: var(--_invalid-edge);
652
+ }
653
+
654
+ [data-ui='select'][data-part='root'][data-invalid]:has(> [data-part='trigger']:focus-visible) {
655
+ box-shadow:
656
+ var(--_invalid-edge),
657
+ var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
658
+ var(--slotted-focus-ring-color, Highlight);
659
+ }
660
+
661
+ [data-ui='select'][data-part='root'][data-variant='plain'][data-invalid]:has(
662
+ > [data-part='trigger']:focus-visible
663
+ ) {
664
+ /* The ring lies inside the box here, over the border and an edge as wide as
665
+ it; the edge reaches past the ring. */
666
+ box-shadow: inset 0 0 0
667
+ calc(
668
+ var(--slotted-focus-ring-offset, 2px) - var(--slotted-control-border-width, 1px) +
669
+ var(--_invalid-edge-width)
670
+ )
671
+ var(--_invalid-edge-color);
672
+ }
673
+
674
+ /*
675
+ * Forced colours remove every box shadow, and the edge with them. The
676
+ * width comes back as an outline inside the border, and a focused invalid
677
+ * control draws its ring double, which a valid ring is not.
678
+ */
679
+ @media (forced-colors: active) {
680
+ [data-ui='select'][data-part='root'][data-invalid] {
681
+ outline: var(--_invalid-edge-width) solid CanvasText;
682
+ outline-offset: calc(
683
+ -1 * (var(--slotted-control-border-width, 1px) + var(--_invalid-edge-width))
684
+ );
685
+ }
686
+
687
+ [data-ui='select'][data-part='root'][data-invalid]:has(> [data-part='trigger']:focus-visible) {
688
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
689
+ var(--slotted-focus-ring-color, Highlight);
690
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
691
+ }
692
+
693
+ [data-ui='select'][data-part='root'][data-variant='plain'][data-invalid]:has(
694
+ > [data-part='trigger']:focus-visible
695
+ ) {
696
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
697
+ var(--slotted-focus-ring-color, Highlight);
698
+ outline-offset: calc(
699
+ -1 * (var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width))
700
+ );
701
+ }
702
+ }
703
+
704
+ /**
705
+ * Read-only stays focusable and legible and must not be mistakable for
706
+ * disabled: the value is still real and still submitted. The ground says
707
+ * "not yours to change" and the ink says nothing at all. Neither a button nor
708
+ * `role="combobox"` has a readonly the platform enforces, so the family
709
+ * refuses the change itself and this is what the refusal looks like.
710
+ */
711
+ [data-ui='select'][data-part='root'][data-readonly] {
712
+ background-color: var(
713
+ --slotted-select-readonly-background,
714
+ var(--slotted-tone-secondary-subtle-hover)
715
+ );
716
+ }
717
+
718
+ [data-ui='select'][data-part='root'][data-readonly] [data-part='arrow'] {
719
+ opacity: 0.5;
720
+ }
721
+
722
+ /**
723
+ * `:disabled` on the trigger is matched beside `data-disabled` on the frame,
724
+ * because a control can be disabled by something that never told it so: a
725
+ * disabled `fieldset` disables every button inside it, and the button has no
726
+ * way to say. The attribute alone left such a control looking operable and
727
+ * answering nothing.
728
+ */
729
+ [data-ui='select'][data-part='root'][data-disabled],
730
+ [data-ui='select'][data-part='root']:has(> [data-part='trigger']:disabled) {
731
+ background-color: var(--slotted-select-disabled-background, var(--slotted-disabled-background));
732
+ border-color: var(--slotted-select-disabled-border-color, var(--slotted-disabled-border));
733
+ /* Nothing a person cannot reach is lifted off the page. */
734
+ box-shadow: none;
735
+ /* Muted to the colour the theme measures rather than faded by opacity: a
736
+ value nobody can read is a field nobody can check. */
737
+ color: var(--slotted-select-disabled-color, var(--slotted-disabled-foreground));
738
+ }
739
+
740
+ [data-ui='select'][data-part='root'][data-disabled] > [data-part='trigger'],
741
+ [data-ui='select'][data-part='root'] > [data-part='trigger']:disabled {
742
+ cursor: not-allowed;
743
+ }
744
+
745
+ [data-ui='select'] > [data-part='clear']:hover:not(:disabled):not([data-disabled]) {
746
+ background-color: var(
747
+ --slotted-select-action-hover-background,
748
+ var(--slotted-tone-secondary-subtle-hover)
749
+ );
750
+ }
751
+
752
+ [data-ui='select'] > [data-part='clear']:active:not(:disabled):not([data-disabled]) {
753
+ background-color: var(
754
+ --slotted-select-action-active-background,
755
+ var(--slotted-tone-secondary-subtle-active)
756
+ );
757
+ }
758
+
759
+ /**
760
+ * The clear button carries its own ring rather than borrowing the frame's.
761
+ * Tabbing from the trigger to it has to move something visible, and a ring
762
+ * around the whole frame would sit still while focus moved inside it.
763
+ */
764
+ [data-ui='select'] > [data-part='clear']:focus-visible {
765
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
766
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
767
+ }
768
+
769
+ /* ── The layer ────────────────────────────────────────────────────────── */
770
+
771
+ /**
772
+ * What `packages/core/src/positioner` writes `position`, `left` and `top`
773
+ * onto, and what slides and fades — the module leaves `transform` alone on
774
+ * purpose. The painted surface is one element in, as `tooltip`, `popover` and
775
+ * `menu` are built.
776
+ */
777
+ [data-ui='select'][data-part='positioner'] {
778
+ --_surface: var(--slotted-select-surface, var(--slotted-surface-raised, #ffffff));
779
+ --_text: var(--slotted-select-content-text, var(--slotted-text, #0f172a));
780
+ --_muted: var(--slotted-select-content-text-muted, var(--slotted-text-muted, #5b6b7f));
781
+ --_border-color: var(
782
+ --slotted-select-content-border-color,
783
+ var(--slotted-border-subtle, #e2e8f0)
784
+ );
785
+ --_border-width: var(--slotted-border-width-thin, 1px);
786
+ --_radius: var(--slotted-select-content-radius, var(--slotted-radius-lg, 8px));
787
+ --_shadow: var(--slotted-select-content-shadow, var(--slotted-shadow-md));
788
+
789
+ /**
790
+ * The highlight is the *stronger* step of the neutral tone, not the one
791
+ * every other subtle surface in this library takes, and the reason is in
792
+ * the dark scheme's numbers: `--slotted-tone-secondary-subtle-hover` is
793
+ * `#18181b` where the raised popup is `#1c1c20`, so the highlight resolved
794
+ * four units **darker** than the ground it sits on and read as a hole
795
+ * rather than as a lift. The first dark capture of this family showed a
796
+ * highlighted option that could not be found. The `-active` step is
797
+ * `#27272a`, eleven units the other way, and it is the only member of the
798
+ * pair that lifts in all three schemes.
799
+ */
800
+ --_highlight: var(--slotted-select-highlight, var(--slotted-tone-secondary-subtle-active));
801
+ --_highlight-active: var(--slotted-select-highlight-active, var(--slotted-tone-accent-surface));
802
+ --_selected-color: var(--slotted-select-selected-color, var(--slotted-tone-accent-text));
803
+
804
+ --_inset-block: var(--slotted-select-content-padding-block, var(--slotted-space-2, 0.25rem));
805
+ --_inset-inline: var(--slotted-select-content-padding-inline, var(--slotted-space-2, 0.25rem));
806
+ --_item-inset: var(--slotted-control-padding-inline-md, 0.8125rem);
807
+ --_item-height: var(--slotted-control-height-md, 34px);
808
+ --_item-radius: var(--slotted-select-item-radius, var(--slotted-control-radius));
809
+ --_content-font-size: var(--slotted-control-font-size-md, 13px);
810
+ --_secondary-font-size: var(--slotted-control-font-size-sm, 12px);
811
+ --_content-icon-size: var(--slotted-control-icon-size-md, 16px);
812
+ --_column-gap: var(--slotted-control-gap-md, 0.5rem);
813
+
814
+ /**
815
+ * The cap for `width="content"`. Measure belongs to `container`, so the
816
+ * family offers no axis for it and still has to stop a `max-content`
817
+ * surface from setting one option the width of the page.
818
+ */
819
+ --_measure: var(--slotted-select-measure, 24rem);
820
+
821
+ /* Only the block inset: the positioner writes `left` and never `right`, and
822
+ `inset-inline-start` would resolve to `right` in a right-to-left document
823
+ and win over the measured value. */
824
+ position: absolute;
825
+ inset-block-start: 0;
826
+
827
+ z-index: calc(var(--slotted-z-index-popover, 1100) + var(--slotted-layer-depth, 0));
828
+ isolation: isolate;
829
+
830
+ box-sizing: border-box;
831
+
832
+ /* The layer is only a frame around the surface; the surface takes the
833
+ pointer, and the frame must not swallow a press meant for the page. */
834
+ pointer-events: none;
835
+
836
+ opacity: 0;
837
+ /**
838
+ * R7: the layer arrives along the axis it opens on — from one space step
839
+ * above its place when it opens downward, from one step below when it
840
+ * opens upward — and never along the inline axis. A `scale` from the
841
+ * corner the positioner named as origin moved the far edge by four per
842
+ * cent of the width, which read as an entrance from the left.
843
+ */
844
+ --_enter-distance: var(--slotted-space-2, 0.25rem);
845
+ translate: 0 calc(-1 * var(--_enter-distance));
846
+ transition:
847
+ opacity var(--slotted-duration-short) var(--slotted-easing-exit),
848
+ translate var(--slotted-duration-short) var(--slotted-easing-exit);
849
+ }
850
+
851
+ [data-ui='select'][data-part='positioner'][data-side='top'] {
852
+ translate: 0 var(--_enter-distance);
853
+ }
854
+
855
+ /* A lateral side has no block direction to arrive from, so it only fades. */
856
+ [data-ui='select'][data-part='positioner']:is([data-side='left'], [data-side='right']) {
857
+ translate: 0 0;
858
+ }
859
+
860
+ /**
861
+ * The entrance is a keyframe animation and not a transition. A transition
862
+ * starts from the style the browser computed last, and the positioner
863
+ * writes `data-side` in the same style update as the family opens the
864
+ * layer, so a transition set out from the closed style of the side the
865
+ * layer had before it had one — the bottom's — whatever side it opens on.
866
+ * A keyframe states where it starts. The exit stays a transition: it
867
+ * leaves from wherever the layer is, on the closed rule's durations.
868
+ */
869
+ [data-ui='select'][data-part='positioner'][data-open] {
870
+ opacity: 1;
871
+ translate: 0 0;
872
+ transition: none;
873
+ }
874
+
875
+ /**
876
+ * Only a layer the positioner placed arrives: it writes `data-positioned`
877
+ * with the side, and a layer written open in static markup — the harness,
878
+ * a docs snippet — is simply there, and is photographed where it stands.
879
+ */
880
+ [data-ui='select'][data-part='positioner'][data-open][data-positioned] {
881
+ animation: slotted-select-enter-down var(--slotted-duration-medium) var(--slotted-easing-enter);
882
+ }
883
+
884
+ [data-ui='select'][data-part='positioner'][data-open][data-positioned][data-side='top'] {
885
+ animation-name: slotted-select-enter-up;
886
+ }
887
+
888
+ [data-ui='select'][data-part='positioner'][data-open][data-positioned]:is(
889
+ [data-side='left'],
890
+ [data-side='right']
891
+ ) {
892
+ animation-name: slotted-select-fade;
893
+ }
894
+
895
+ @keyframes slotted-select-enter-down {
896
+ from {
897
+ opacity: 0;
898
+ translate: 0 calc(-1 * var(--_enter-distance));
899
+ }
900
+ }
901
+
902
+ @keyframes slotted-select-enter-up {
903
+ from {
904
+ opacity: 0;
905
+ translate: 0 var(--_enter-distance);
906
+ }
907
+ }
908
+
909
+ @keyframes slotted-select-fade {
910
+ from {
911
+ opacity: 0;
912
+ }
913
+ }
914
+
915
+ /* Stillness keeps the fade and drops the movement (R7). */
916
+ @media (prefers-reduced-motion: reduce) {
917
+ [data-ui='select'][data-part='positioner'],
918
+ [data-ui='select'][data-part='positioner'][data-side='top'],
919
+ [data-ui='select'][data-part='positioner'][data-side='left'],
920
+ [data-ui='select'][data-part='positioner'][data-side='right'] {
921
+ translate: 0 0;
922
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
923
+ }
924
+
925
+ [data-ui='select'][data-part='positioner'][data-open][data-positioned],
926
+ [data-ui='select'][data-part='positioner'][data-open][data-positioned][data-side='top'],
927
+ [data-ui='select'][data-part='positioner'][data-open][data-positioned][data-side='left'],
928
+ [data-ui='select'][data-part='positioner'][data-open][data-positioned][data-side='right'] {
929
+ animation-name: slotted-select-fade;
930
+ }
931
+ }
932
+
933
+ /* The anchor scrolled out of its own clipper. The popup is still open and
934
+ its options still belong to a control the reader can no longer see. */
935
+ [data-ui='select'][data-part='positioner'][data-detached] {
936
+ visibility: hidden;
937
+ }
938
+
939
+ /**
940
+ * `content` is the default: never narrower than the control, and wider
941
+ * when an option needs it, up to the measure and the available width, so a
942
+ * narrow control does not wrap its option labels. `anchor` pins the popup to
943
+ * the control's width exactly.
944
+ */
945
+ [data-ui='select'][data-part='positioner'][data-width='anchor'] {
946
+ inline-size: var(--slotted-positioner-anchor-width, auto);
947
+ }
948
+
949
+ [data-ui='select'][data-part='positioner'][data-width='content'] {
950
+ inline-size: max-content;
951
+ max-inline-size: min(var(--_measure), var(--slotted-positioner-available-width, 100vw));
952
+ /* Never narrower than the control it belongs to: a popup that shrank under
953
+ its own anchor reads as a second, smaller control. */
954
+ min-inline-size: var(--slotted-positioner-anchor-width, auto);
955
+ }
956
+
957
+ [data-ui='select'][data-part='positioner'][data-size='sm'] {
958
+ --_item-inset: var(--slotted-control-padding-inline-sm, 0.625rem);
959
+ --_item-height: var(--slotted-control-height-sm, 30px);
960
+ --_content-font-size: var(--slotted-control-font-size-sm, 12px);
961
+ --_secondary-font-size: var(--slotted-font-size-xs, 0.75rem);
962
+ --_content-icon-size: var(--slotted-control-icon-size-sm, 14px);
963
+ --_column-gap: var(--slotted-control-gap-sm, 0.375rem);
964
+ }
965
+
966
+ [data-ui='select'][data-part='positioner'][data-size='lg'] {
967
+ --_item-inset: var(--slotted-control-padding-inline-lg, 1rem);
968
+ --_item-height: var(--slotted-control-height-lg, 38px);
969
+ --_content-font-size: var(--slotted-control-font-size-lg, 14px);
970
+ --_secondary-font-size: var(--slotted-control-font-size-sm, 12px);
971
+ --_content-icon-size: var(--slotted-control-icon-size-lg, 18px);
972
+ --_column-gap: var(--slotted-control-gap-lg, 0.5rem);
973
+ }
974
+
975
+ /* ── The surface ──────────────────────────────────────────────────────── */
976
+
977
+ /**
978
+ * Three columns, and the height the positioner says is left. Radix, Base UI
979
+ * and React Aria each publish the remaining height and none of them bounds
980
+ * itself by default, so a list of a hundred options runs off the bottom of
981
+ * the screen. `overscroll-behavior` keeps the page still once the surface has
982
+ * scrolled to its end.
983
+ */
984
+ [data-ui='select'] > [data-part='content'] {
985
+ background: var(--_surface);
986
+ border: var(--_border-width) solid var(--_border-color);
987
+ border-radius: var(--_radius);
988
+ box-shadow: var(--_shadow);
989
+ box-sizing: border-box;
990
+ color: var(--_text);
991
+ font-family: var(--slotted-font-family-sans);
992
+ font-size: var(--_content-font-size);
993
+ max-block-size: calc(
994
+ var(--slotted-positioner-available-height, 100vh) - var(--slotted-space-5, 0.75rem)
995
+ );
996
+ outline: none;
997
+ pointer-events: auto;
998
+ }
999
+
1000
+ /**
1001
+ * The three columns and the scroll, on whichever element holds the options.
1002
+ *
1003
+ * Without a search field that element is the surface itself, which is where
1004
+ * it has always been. With one the surface becomes a column of two — the
1005
+ * field, then the list — and the list is what scrolls: a field that scrolled
1006
+ * away with its own results is a field a reader loses on the fourth row, and
1007
+ * every implementation surveyed on 2026-09-08 that puts the field in the
1008
+ * popup keeps it still.
1009
+ *
1010
+ * `overscroll-behavior` keeps the page still once the scroller has reached
1011
+ * its end.
1012
+ */
1013
+ [data-ui='select'] > [data-part='content']:not([data-search]),
1014
+ [data-ui='select'] [data-part='list'] {
1015
+ display: grid;
1016
+ grid-template-columns: auto minmax(0, 1fr) auto;
1017
+ overflow-y: auto;
1018
+ overscroll-behavior: contain;
1019
+ padding-block: var(--_inset-block);
1020
+ padding-inline: var(--_inset-inline);
1021
+ }
1022
+
1023
+ [data-ui='select'] > [data-part='content'][data-search] {
1024
+ display: flex;
1025
+ flex-direction: column;
1026
+ /* The field draws to the surface's own edges and the list keeps the inset,
1027
+ so each of the two states its own padding rather than sharing one. */
1028
+ padding: 0;
1029
+ }
1030
+
1031
+ /* `auto` as the flex basis measures the whole list and refuses to shrink;
1032
+ the surface is what holds the height, and the list is what gives. */
1033
+ [data-ui='select'] [data-part='list'] {
1034
+ flex: 1 1 auto;
1035
+ min-block-size: 0;
1036
+ }
1037
+
1038
+ /* ── The search field ─────────────────────────────────────────────────── */
1039
+
1040
+ /**
1041
+ * The row the query is typed into, at the top of the popup and only under
1042
+ * `search`.
1043
+ *
1044
+ * It is in the popup rather than in the control, and that is the whole of
1045
+ * what keeps this family apart from `combobox`: a combobox's typed text is a
1046
+ * value it commits, and this query is transient — it filters, it is emptied
1047
+ * when an option is taken, and the control goes on showing the chosen label
1048
+ * the entire time. `docs/component-features.md` §6 says the control takes no
1049
+ * typed text, and it still does not.
1050
+ *
1051
+ * A row the height of an option, so the field and the first result keep one
1052
+ * rhythm, with the rule under it drawn from the border scale rather than
1053
+ * from a shadow — the same rule the `chosen` band draws above the list.
1054
+ */
1055
+ [data-ui='select'] [data-part='search'] {
1056
+ align-items: center;
1057
+ border-block-end: var(--_border-width) solid var(--_border-color);
1058
+ box-sizing: border-box;
1059
+ display: flex;
1060
+ flex: 0 0 auto;
1061
+ gap: var(--_column-gap);
1062
+ min-block-size: var(--_item-height);
1063
+ padding-block: var(--slotted-space-2, 0.25rem);
1064
+ /* The list is inset from the surface and each option is inset again inside
1065
+ it, so a row that took only the option's inset put its glyph four pixels
1066
+ to the start of every label under it. The two insets are added here, and
1067
+ the magnifier opens the same column the labels do. */
1068
+ padding-inline: calc(var(--_inset-inline) + var(--_item-inset));
1069
+ }
1070
+
1071
+ /**
1072
+ * The magnifier. It is what makes the row a field rather than a heading: a
1073
+ * bare line of placeholder text at the top of a panel reads as a title, and
1074
+ * a reader does not type into a title.
1075
+ */
1076
+ [data-ui='select'] [data-part='search-icon'] {
1077
+ --slotted-icon-size: var(--_content-icon-size);
1078
+
1079
+ align-items: center;
1080
+ block-size: var(--_content-icon-size);
1081
+ color: var(--slotted-select-search-icon-color, var(--_muted));
1082
+ display: flex;
1083
+ flex: 0 0 auto;
1084
+ inline-size: var(--_content-icon-size);
1085
+ justify-content: center;
1086
+ pointer-events: none;
1087
+ }
1088
+
1089
+ /**
1090
+ * The field itself, which gives up every frame of its own: the surface is
1091
+ * already a bordered panel, and a second border a step inside it would draw
1092
+ * a box inside a box for one line of text.
1093
+ *
1094
+ * **No focus ring.** The field takes focus as the popup opens and holds it
1095
+ * until the popup closes — there is no state in which this layer is open and
1096
+ * this element is not focused — so a ring here would be a permanent border
1097
+ * rather than a state, and the one thing on the surface that never changes
1098
+ * would be the loudest. What says where focus is, is the caret.
1099
+ */
1100
+ [data-ui='select'] [data-part='search-input'] {
1101
+ background-color: transparent;
1102
+ border: 0;
1103
+ color: inherit;
1104
+ flex: 1 1 auto;
1105
+ font: inherit;
1106
+ /* The intrinsic width of an `input` is about twenty characters, and a flex
1107
+ item refuses to shrink below it without this. */
1108
+ min-inline-size: 0;
1109
+ outline: none;
1110
+ padding: 0;
1111
+ text-overflow: ellipsis;
1112
+ }
1113
+
1114
+ [data-ui='select'] [data-part='search-input']::placeholder {
1115
+ color: var(--slotted-select-placeholder-color, var(--_muted));
1116
+ text-overflow: ellipsis;
1117
+ }
1118
+
1119
+ /**
1120
+ * Everything that is chosen, at the top of the popup (SL10).
1121
+ *
1122
+ * The control holds one line, so twelve chosen values are three chips and a
1123
+ * count; here they are all of them. The band wraps and takes about two rows
1124
+ * of its own before it scrolls, and it scrolls **itself** — the list under it
1125
+ * keeps its own scroll, and `overscroll-behavior` stops one from carrying the
1126
+ * other once it reaches its end.
1127
+ *
1128
+ * It is stuck to the top of the scrollport rather than laid above it: the
1129
+ * surface is the scroller as well as the box, so a band that merely came
1130
+ * first would scroll away with the options. The negative insets take it out
1131
+ * to the surface's own edges, which is where the rule between it and the list
1132
+ * has to end — a rule from the border scale, not a shadow.
1133
+ */
1134
+ [data-ui='select'] [data-part='chosen'] {
1135
+ background: var(--_surface);
1136
+ border-block-end: var(--_border-width) solid var(--_border-color);
1137
+ box-sizing: border-box;
1138
+ display: flex;
1139
+ flex-wrap: wrap;
1140
+ gap: var(--slotted-space-2, 0.25rem);
1141
+ grid-column: 1 / -1;
1142
+ /* Its own padding stands where the surface's would, so nothing shows above
1143
+ it as the list scrolls under. */
1144
+ inset-block-start: calc(-1 * var(--_inset-block));
1145
+ margin-block: calc(-1 * var(--_inset-block)) var(--_inset-block);
1146
+ margin-inline: calc(-1 * var(--_inset-inline));
1147
+ /**
1148
+ * Two rows of chips, the gap between them, and the air above the first.
1149
+ * Two *whole* rows: a chip drawn to half its height is not an affordance,
1150
+ * it is a chip that looks broken. What says there is more is the scrollbar
1151
+ * and the rule under it. The row is the target minimum, which is the floor
1152
+ * a chip's own remove control puts under it everywhere else (TG2/SL8), so
1153
+ * a chip at the small step never overruns the cap.
1154
+ */
1155
+ max-block-size: calc(
1156
+ 2 * var(--slotted-target-min, 24px) + var(--slotted-space-2, 0.25rem) + var(--_inset-block)
1157
+ );
1158
+ min-inline-size: 0;
1159
+ overflow-y: auto;
1160
+ overscroll-behavior: contain;
1161
+ padding-block: var(--_inset-block);
1162
+ padding-inline: var(--_inset-inline);
1163
+ position: sticky;
1164
+ z-index: 1;
1165
+ }
1166
+
1167
+ /**
1168
+ * The surface never takes a ring. Focus stays on the control for the whole
1169
+ * interaction — the highlight is `aria-activedescendant`, not focus — so a
1170
+ * ring here would draw a box around something nobody is operating.
1171
+ */
1172
+ [data-ui='select'] > [data-part='content']:focus-visible {
1173
+ outline: none;
1174
+ }
1175
+
1176
+ /* ── Groups ───────────────────────────────────────────────────────────── */
1177
+
1178
+ /**
1179
+ * A group spans every column and inherits them, so its options keep the
1180
+ * surface's columns instead of starting a second set. Without the subgrid a
1181
+ * group would align its own labels and disagree with the options outside it.
1182
+ */
1183
+ [data-ui='select'] [data-part='group'] {
1184
+ display: grid;
1185
+ grid-column: 1 / -1;
1186
+ grid-template-columns: subgrid;
1187
+ }
1188
+
1189
+ /**
1190
+ * An option the query rejected, and a group whose options all went. Both
1191
+ * carry `hidden` — Angular hides each rejected option rather than leaving the
1192
+ * list, and both bindings hide an emptied group — and `display: grid` on
1193
+ * either beats the user agent's own `[hidden] { display: none }`, because an
1194
+ * author rule wins over the user agent sheet. Without this the search
1195
+ * filtered nothing the reader could see.
1196
+ */
1197
+ [data-ui='select'] [data-part='item'][hidden],
1198
+ [data-ui='select'] [data-part='group'][hidden] {
1199
+ display: none;
1200
+ }
1201
+
1202
+ [data-ui='select'] [data-part='group-label'] {
1203
+ color: var(--_muted);
1204
+ font-size: var(--_secondary-font-size);
1205
+ font-weight: var(--slotted-font-weight-semibold, 600);
1206
+ grid-column: 1 / -1;
1207
+ line-height: var(--slotted-line-height-sm, 1.45);
1208
+ padding-block: var(--slotted-space-2, 0.25rem);
1209
+ padding-inline: var(--_item-inset);
1210
+ }
1211
+
1212
+ /* ── Options ──────────────────────────────────────────────────────────── */
1213
+
1214
+ /**
1215
+ * `role="option"` on a `div`. Unlike `menu`'s item there is no native
1216
+ * element to keep: the listbox pattern's option is not a button, it is not
1217
+ * separately focusable — DOM focus stays on the control — and a `button`
1218
+ * here would put a widget inside a widget and add a tab stop the pattern
1219
+ * excludes by name.
1220
+ *
1221
+ * `min-block-size` rather than a height, because an option with a
1222
+ * description is two lines tall and still has to clear the target minimum.
1223
+ */
1224
+ [data-ui='select'] [data-part='item'] {
1225
+ align-content: center;
1226
+ border-radius: var(--_item-radius);
1227
+ box-sizing: border-box;
1228
+ /**
1229
+ * The hand (SL11). Nine of the twelve implementations surveyed draw it;
1230
+ * the three that draw an arrow are the three that follow the native
1231
+ * `select`, whose popup the operating system paints. A row that commits a
1232
+ * value and closes the surface is a press. `menu` kept the arrow for a
1233
+ * while on the argument that a menu item follows the platform's own
1234
+ * menus, and dropped it: its surface is a `div` on the page as much as
1235
+ * this one is.
1236
+ */
1237
+ cursor: pointer;
1238
+ display: grid;
1239
+ grid-column: 1 / -1;
1240
+ grid-template-columns: subgrid;
1241
+ /**
1242
+ * The line box belongs to the option and every cell inherits it. `1lh` on a
1243
+ * mark resolves against the mark's *own* line-height, so a mark inside a
1244
+ * row that left the line box at `normal` measures fifteen pixels while the
1245
+ * label beside it measures nineteen and a half — and start-aligned in the
1246
+ * same row, the mark sits two pixels above the text it annotates.
1247
+ */
1248
+ line-height: var(--slotted-line-height-md, 1.5);
1249
+ min-block-size: var(--_item-height);
1250
+ padding-block: var(--slotted-space-2, 0.25rem);
1251
+ padding-inline: var(--_item-inset);
1252
+ row-gap: var(--slotted-space-1, 0.125rem);
1253
+ /* A press that lands on an option must not also select the label's text,
1254
+ which is what a double-click on a list otherwise does. */
1255
+ user-select: none;
1256
+ transition:
1257
+ background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
1258
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
1259
+ color var(--slotted-control-transition-duration, var(--slotted-duration-short))
1260
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
1261
+ }
1262
+
1263
+ /**
1264
+ * The highlight. It is `data-current` rather than a hover or focus selector,
1265
+ * because the highlight is where the reader is and the pointer and the
1266
+ * keyboard both move it: entering an option with the pointer sets
1267
+ * `aria-activedescendant` to it, so exactly one option is highlighted
1268
+ * whichever device is driving, and the state a screen reader announces and
1269
+ * the state the eye sees are the same state.
1270
+ */
1271
+ [data-ui='select'] [data-part='item'][data-current] {
1272
+ background-color: var(--_highlight);
1273
+ /**
1274
+ * A ring as well as a ground, because the ground alone carries the whole
1275
+ * signal and eleven units of lift is not enough to carry it. The APG's own
1276
+ * select-only example marks its current option with an outline for the
1277
+ * same reason: nothing on this surface holds focus — DOM focus is on the
1278
+ * control — so there is no focus ring anywhere to say where the reader is.
1279
+ *
1280
+ * An inset shadow rather than an outline, so it follows the option's own
1281
+ * radius and never overflows the surface's rounded edge, and one border
1282
+ * width rather than two, because it is a cursor and not a focus indicator.
1283
+ */
1284
+ box-shadow: inset 0 0 0 var(--slotted-border-width-thin, 1px)
1285
+ var(--slotted-select-highlight-ring, var(--slotted-tone-accent-border));
1286
+ }
1287
+
1288
+ [data-ui='select'] [data-part='item']:not([data-disabled]):active {
1289
+ background-color: var(--_highlight-active);
1290
+ }
1291
+
1292
+ /**
1293
+ * Chosen, which is not the same as highlighted. The check in the last column
1294
+ * is the primary signal — it survives forced colours, where every tone ground
1295
+ * becomes `Canvas` — and the accent ink is the secondary one, so the row a
1296
+ * reader is looking for is findable at a glance in a list of forty.
1297
+ */
1298
+ [data-ui='select'] [data-part='item'][data-selected] {
1299
+ color: var(--_selected-color);
1300
+ }
1301
+
1302
+ /**
1303
+ * Two chosen neighbours are one block (LB2). Every item keeps its own
1304
+ * radius, so two selected siblings painted with the same ground met at a
1305
+ * waist — four rounded corners inside the block — and a multiple selection
1306
+ * read as a column of separate chips. The second gives up its top corners
1307
+ * and the first its bottom ones, so the block is rounded only where the
1308
+ * selection begins and ends. `select` and `combobox` draw the same ground
1309
+ * and answer the same way; `selection-block.test.mjs` holds the three to it.
1310
+ */
1311
+ [data-ui='select'] [data-part='item'][data-selected] + [data-part='item'][data-selected] {
1312
+ border-start-end-radius: 0;
1313
+ border-start-start-radius: 0;
1314
+ }
1315
+
1316
+ [data-ui='select'] [data-part='item'][data-selected]:has(+ [data-part='item'][data-selected]) {
1317
+ border-end-end-radius: 0;
1318
+ border-end-start-radius: 0;
1319
+ }
1320
+
1321
+ [data-ui='select'] [data-part='item'][data-selected] > [data-part='label'] {
1322
+ font-weight: var(--slotted-font-weight-medium, 500);
1323
+ }
1324
+
1325
+ /**
1326
+ * A disabled option is announced, keeps its place and cannot be chosen. It
1327
+ * therefore has to answer the highlight with something, and answer it more
1328
+ * quietly than an option that would do anything: the ground is the theme's
1329
+ * disabled surface rather than the highlight, and the text stays legible
1330
+ * instead of being faded toward it.
1331
+ */
1332
+ [data-ui='select'] [data-part='item'][data-disabled] {
1333
+ color: var(--slotted-disabled-foreground, #64748b);
1334
+ cursor: not-allowed;
1335
+ }
1336
+
1337
+ /**
1338
+ * The highlight can land on an option nobody can choose, because a disabled
1339
+ * option keeps its place in the collection and the pattern requires it to be
1340
+ * announced. It must not wear the accent cursor while it is there: an accent
1341
+ * ring on a row that answers nothing says "act here" to the one reader who
1342
+ * cannot. The ground and the ring both step down to the theme's disabled
1343
+ * pair, and the row stays legible rather than being faded toward it.
1344
+ */
1345
+ /**
1346
+ * The highlight can land on an option nobody can choose, because a disabled
1347
+ * option keeps its place in the collection and the pattern requires it to be
1348
+ * announced.
1349
+ *
1350
+ * **The ground stays the highlight's.** Painting the theme's disabled surface
1351
+ * here was the first answer and it was wrong in the dark scheme, for the same
1352
+ * reason the highlight itself was: `--slotted-disabled-background` is
1353
+ * `#18181b` where the raised popup is `#1c1c20`, so the cursor *sank* on a
1354
+ * disabled row and rose on every other one — a reader arrowing down the list
1355
+ * would watch it disappear. A cursor lifts, always. What the row cannot do is
1356
+ * said by the ink and by the ring, which both step down to the disabled pair.
1357
+ */
1358
+ [data-ui='select'] [data-part='item'][data-disabled][data-current] {
1359
+ box-shadow: inset 0 0 0 var(--slotted-border-width-thin, 1px)
1360
+ var(--slotted-disabled-border, #cbd5e1);
1361
+ color: var(--slotted-disabled-foreground, #64748b);
1362
+ }
1363
+
1364
+ /* ── The cells ────────────────────────────────────────────────────────── */
1365
+
1366
+ /**
1367
+ * One line box tall, so a mark beside a label that wrapped sits on the first
1368
+ * line rather than in the middle of the option — and, because the option
1369
+ * states the line box every cell inherits, exactly as tall as the line it
1370
+ * sits beside.
1371
+ */
1372
+ [data-ui='select'] :is([data-part='indicator'], [data-part='icon']) {
1373
+ block-size: 1lh;
1374
+ }
1375
+
1376
+ /**
1377
+ * Column one, and the reason an option with artwork and one without line
1378
+ * their labels up: the column is as wide as the content icon step whether or
1379
+ * not a given option fills it.
1380
+ *
1381
+ * The box holds content and paints none: a `Glyph` in an `Icon` box, or
1382
+ * artwork of the consumer's own, centred and sized from the column through
1383
+ * `--slotted-icon-size`. No stylesheet fills it.
1384
+ */
1385
+ [data-ui='select'] [data-part='icon'] {
1386
+ --slotted-icon-size: var(--_content-icon-size);
1387
+
1388
+ align-items: center;
1389
+ display: flex;
1390
+ grid-column: 1;
1391
+ grid-row: 1;
1392
+ inline-size: var(--_content-icon-size);
1393
+ justify-content: center;
1394
+ justify-self: center;
1395
+ margin-inline-end: var(--_column-gap);
1396
+ }
1397
+
1398
+ /* Column two, and the only column that gives way. It is scoped to the option
1399
+ rather than to the family: a chip in the control carries a `label` of its
1400
+ own, and this rule would have put it in the popup's second column (SL4). */
1401
+ [data-ui='select'] [data-part='item'] > [data-part='label'] {
1402
+ grid-column: 2;
1403
+ grid-row: 1;
1404
+ min-inline-size: 0;
1405
+ /**
1406
+ * A label longer than the popup wraps to two lines and then truncates,
1407
+ * with the whole string still in the DOM. Radix, Base UI and Ark all let it
1408
+ * wrap to as many lines as it takes, so one translated option can be five
1409
+ * lines tall and the list stops being scannable; Material truncates at one
1410
+ * line and loses the text. Two lines is the compromise, and the second one
1411
+ * is where a name and its qualifier fit.
1412
+ */
1413
+ display: -webkit-box;
1414
+ -webkit-box-orient: vertical;
1415
+ -webkit-line-clamp: 2;
1416
+ overflow: hidden;
1417
+ /* A sixty-character identifier with no spaces breaks rather than escaping
1418
+ the surface, which is what it does in two of the surveyed libraries. */
1419
+ overflow-wrap: anywhere;
1420
+ }
1421
+
1422
+ [data-ui='select'] [data-part='description'] {
1423
+ color: var(--_muted);
1424
+ font-size: var(--_secondary-font-size);
1425
+ grid-column: 2;
1426
+ grid-row: 2;
1427
+ line-height: var(--slotted-line-height-sm, 1.45);
1428
+ min-inline-size: 0;
1429
+ overflow-wrap: anywhere;
1430
+ }
1431
+
1432
+ [data-ui='select'] [data-part='item'][data-disabled] > [data-part='description'] {
1433
+ color: inherit;
1434
+ }
1435
+
1436
+ /**
1437
+ * Column three, and the mark that says an option was chosen. The box is
1438
+ * reserved whether or not the check is drawn, so a list does not shift by
1439
+ * sixteen pixels as the reader moves through it.
1440
+ */
1441
+ [data-ui='select'] [data-part='indicator'] {
1442
+ --slotted-icon-size: var(--_content-icon-size);
1443
+
1444
+ align-items: center;
1445
+ display: flex;
1446
+ grid-column: 3;
1447
+ grid-row: 1;
1448
+ inline-size: var(--_content-icon-size);
1449
+ justify-content: center;
1450
+ justify-self: end;
1451
+ margin-inline-start: var(--_column-gap);
1452
+ opacity: 0;
1453
+ /* The check appears rather than blinking: the one movement inside the
1454
+ popup, and it is what confirms the press the reader just made. */
1455
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-standard);
1456
+ }
1457
+
1458
+ [data-ui='select'] [data-part='item'][data-selected] > [data-part='indicator'] {
1459
+ opacity: 1;
1460
+ }
1461
+
1462
+ /* ── Separator, empty, status ─────────────────────────────────────────── */
1463
+
1464
+ [data-ui='select'] [data-part='separator'] {
1465
+ background-color: var(--slotted-select-separator-color, var(--_border-color));
1466
+ block-size: var(--_border-width);
1467
+ border: 0;
1468
+ grid-column: 1 / -1;
1469
+ margin-block: var(--slotted-space-2, 0.25rem);
1470
+ margin-inline: 0;
1471
+ }
1472
+
1473
+ /**
1474
+ * A list with nothing in it, and a list still waiting. Both keep the
1475
+ * surface's measure rather than collapsing to a two-pixel box, which is what
1476
+ * a consumer sees flash when the state is not painted at all. React Aria
1477
+ * refuses to open on an empty collection and Radix paints nothing; the popup
1478
+ * here opens onto a sentence.
1479
+ */
1480
+ [data-ui='select'] :is([data-part='empty'], [data-part='status']) {
1481
+ color: var(--_muted);
1482
+ /**
1483
+ * Its own bidi run, ordered by its own content. "3 results" is a European
1484
+ * number, a neutral space and a Latin word, so in a right-to-left document
1485
+ * the space resolves to the paragraph and the two runs swap: the first
1486
+ * right-to-left capture of this family printed "results 3". `plaintext`
1487
+ * takes the direction from the sentence's own first strong character, so an
1488
+ * English count reads as itself and a translated one reads as itself — the
1489
+ * same repair `text-field` makes for an adornment, and for the same reason.
1490
+ * These two parts are centred, so taking their own direction costs no
1491
+ * alignment; an option's label is the consumer's content and keeps the
1492
+ * document's direction unless the consumer says otherwise.
1493
+ */
1494
+ unicode-bidi: plaintext;
1495
+ grid-column: 1 / -1;
1496
+ /**
1497
+ * Wide enough that a one-word message does not draw a popup narrower than a
1498
+ * word, and never wider than the popup it is in: at `width="anchor"` the
1499
+ * surface is exactly the control's width, and a bare `12rem` on a child
1500
+ * overflows the grid the moment the control is narrower than that.
1501
+ */
1502
+ min-inline-size: min(12rem, 100%);
1503
+ padding-block: var(--slotted-space-5, 0.75rem);
1504
+ padding-inline: var(--_item-inset);
1505
+ text-align: center;
1506
+ }
1507
+
1508
+ /**
1509
+ * Under `search` the status row is also the live region that announces how
1510
+ * many options survived the query, so it is present on every open popup and
1511
+ * empty on most of them. A row with no text takes no space; a row with text
1512
+ * takes the same space as the empty message beside it. `combobox` draws the
1513
+ * count the same way, in the same place, for the same reason.
1514
+ */
1515
+ [data-ui='select'] [data-part='status']:empty {
1516
+ display: none;
1517
+ }
1518
+
1519
+ /**
1520
+ * The wait, as the shape of the list that is coming. Three rows the height of
1521
+ * an option rather than one sentence, so the surface a reader is looking at
1522
+ * while the answer travels is the surface they will go on looking at
1523
+ * (SL6/CB5). The bars are `skeleton/skeleton.css`'s — the family places them
1524
+ * and states nothing about how a placeholder is painted.
1525
+ *
1526
+ * The three widths descend, because three identical bars read as a table and
1527
+ * a list of labels is ragged. They are the consumer's nothing, so they are
1528
+ * hidden from assistive technology: the `status` row beside them is the live
1529
+ * region and it already says what is happening.
1530
+ */
1531
+ [data-ui='select'] [data-part='placeholder'] {
1532
+ display: grid;
1533
+ /* The surface is a grid of option columns; the rows span all of them, as
1534
+ the status and empty rows do. */
1535
+ grid-column: 1 / -1;
1536
+ padding-block: var(--slotted-space-2, 0.25rem);
1537
+ padding-inline: var(--_item-inset);
1538
+ }
1539
+
1540
+ [data-ui='select'] [data-part='placeholder'] > [data-ui='skeleton'] {
1541
+ align-self: center;
1542
+ }
1543
+
1544
+ /**
1545
+ * Each row is an option: the same minimum height, so three of them measure
1546
+ * what three options measure and the popup does not resize when they land.
1547
+ */
1548
+ [data-ui='select'] [data-part='placeholder'] > * {
1549
+ align-items: center;
1550
+ display: flex;
1551
+ min-block-size: var(--_item-height);
1552
+ }
1553
+
1554
+ [data-ui='select'] [data-part='placeholder'] > :nth-child(2) [data-ui='skeleton'] {
1555
+ inline-size: 78%;
1556
+ }
1557
+
1558
+ [data-ui='select'] [data-part='placeholder'] > :nth-child(3) [data-ui='skeleton'] {
1559
+ inline-size: 56%;
1560
+ }
1561
+
1562
+ /**
1563
+ * The status row while an answer is travelling. It no longer draws a pulsing
1564
+ * dot of its own: the control carries the ring and the popup carries the
1565
+ * three bars, and a third repeating motion beside them was one answer too
1566
+ * many for one wait (SL6).
1567
+ */
1568
+
1569
+ [data-ui='select'] > [data-part='content'][data-loading] {
1570
+ cursor: progress;
1571
+ }
1572
+
1573
+ /* ── Forced colours ───────────────────────────────────────────────────── */
1574
+
1575
+ /**
1576
+ * The one thing the theme cannot reach. Every tone surface becomes `Canvas`
1577
+ * under forced colours, so the highlight — which is this family's answer to
1578
+ * "which option am I on", not decoration — would disappear and leave a reader
1579
+ * with no way to tell.
1580
+ */
1581
+ @media (forced-colors: active) {
1582
+ [data-ui='select'] [data-part='item'][data-current] {
1583
+ background-color: Highlight;
1584
+ color: HighlightText;
1585
+ forced-color-adjust: none;
1586
+ }
1587
+
1588
+ [data-ui='select'] [data-part='item'][data-disabled][data-current] {
1589
+ background-color: Highlight;
1590
+ color: GrayText;
1591
+ }
1592
+ }
1593
+ }