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

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,674 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * A list of options the reader chooses from, and the primitive `select`,
4
+ * `combobox` and the pickers are built on.
5
+ *
6
+ * ┌─ root ─ role="listbox" ─ scrolls ─ one tab stop ──────────────┐
7
+ * │ ┌─ canvas ─ role="presentation" ─ only while windowed ───┐ │
8
+ * │ │ ┌─ group ─ role="group" ────────────────────────┐ │ │
9
+ * │ │ │ group-label │ │ │
10
+ * │ │ │ option ▸ indicator or icon │ label │ meta │ │ │
11
+ * │ │ │ description │ │ │
12
+ * │ │ └───────────────────────────────────────────────┘ │ │
13
+ * │ │ empty · loader │ │
14
+ * │ └────────────────────────────────────────────────────────┘ │
15
+ * └───────────────────────────────────────────────────────────────┘
16
+ *
17
+ * ── Why the surface is a grid and everything inside it is a subgrid ──
18
+ *
19
+ * The three columns — the mark, the label, the value — belong to the whole
20
+ * listbox rather than to one option, so `group`, `canvas` and `option` each
21
+ * pass through them with `grid-template-columns: subgrid`. Labels line up
22
+ * across every group, the value column is as wide as the widest value and
23
+ * never narrower, and the label column is the only one that gives way. A flex
24
+ * row per option — which is what every surveyed implementation draws — lets a
25
+ * long translated label eat the value beside it, and leaves two options'
26
+ * labels out of line whenever one has an icon and the other does not.
27
+ *
28
+ * The columns carry no `column-gap`: a gap would be drawn between two tracks
29
+ * even when the first is empty, so a listbox with no marks at all would open
30
+ * with a gutter down its start edge. Each optional cell carries its own
31
+ * margin instead, and a column nothing occupies collapses to nothing. That is
32
+ * `menu`'s rule, and the two families are the same row under two roles.
33
+ *
34
+ * ── Two layouts that are not the subgrid ────────────────────────────
35
+ *
36
+ * `data-layout="grid"` puts `columns` options across, so the root's tracks
37
+ * become the columns and an option owns its own three. `data-orientation
38
+ * ="horizontal"` lays the options along the inline axis and scrolls that way.
39
+ * Both are declared by the family from its own props; neither changes what an
40
+ * option is made of.
41
+ *
42
+ * ── Density ─────────────────────────────────────────────────────────
43
+ *
44
+ * An option is a control, so its height, inset, type, glyph and gap all read
45
+ * control geometry. A dense listbox holds more options and is the same
46
+ * component; only the surface's own inset, radius and shadow come off the
47
+ * space, radius and shadow scales.
48
+ */
49
+ [data-ui='listbox'] {
50
+ --_surface: var(--slotted-listbox-surface, var(--slotted-surface, #ffffff));
51
+ --_text: var(--slotted-listbox-text, var(--slotted-text, #0f172a));
52
+ --_muted: var(--slotted-listbox-text-muted, var(--slotted-text-muted, #5b6b7f));
53
+ --_border-color: var(--slotted-listbox-border-color, var(--slotted-control-border-color));
54
+ --_border-width: var(--slotted-border-width-thin, 1px);
55
+ --_radius: var(--slotted-listbox-radius, var(--slotted-radius-md, 6px));
56
+ --_shadow: var(--slotted-listbox-shadow, var(--slotted-control-shadow));
57
+
58
+ /**
59
+ * One quiet ground for hover and for the option the keyboard is on, and the
60
+ * two are told apart by the ring rather than by two greys. A second, darker
61
+ * grey was tried first and read the wrong way round: the pointer's own
62
+ * feedback shouted louder than the keyboard's position. What separates them
63
+ * now is that the active option paints nothing at all until the keyboard is
64
+ * somewhere it can be — see the rule below.
65
+ */
66
+ --_highlight: var(--slotted-listbox-highlight, var(--slotted-tone-secondary-subtle-hover));
67
+ --_highlight-active: var(
68
+ --slotted-listbox-highlight-active,
69
+ var(--slotted-tone-secondary-subtle-active)
70
+ );
71
+ --_selected-surface: var(
72
+ --slotted-listbox-selected-surface,
73
+ var(--slotted-tone-accent-surface)
74
+ );
75
+ --_selected-text: var(--slotted-listbox-selected-text, var(--slotted-tone-accent-text));
76
+
77
+ --_inset-block: var(--slotted-listbox-padding-block, var(--slotted-space-2, 0.25rem));
78
+ --_inset-inline: var(--slotted-listbox-padding-inline, var(--slotted-space-2, 0.25rem));
79
+ --_option-inset: var(--slotted-control-padding-inline-md, 0.8125rem);
80
+ --_option-height: var(--slotted-control-height-md, 34px);
81
+ --_option-radius: var(--slotted-listbox-option-radius, var(--slotted-control-radius));
82
+ --_font-size: var(--slotted-control-font-size-md, 13px);
83
+ --_secondary-font-size: var(--slotted-control-font-size-sm, 12px);
84
+ --_icon-size: var(--slotted-control-icon-size-md, 16px);
85
+ --_column-gap: var(--slotted-control-gap-md, 0.5rem);
86
+
87
+ /**
88
+ * How many options tall the box is, and how many across. `<select size>`
89
+ * under another name: a listbox with no bound is a listbox that pushes the
90
+ * page down as its data arrives, and a windowed one has no viewport at all.
91
+ * The family writes both per instance; the tokens are what a theme retunes.
92
+ */
93
+ --_rows: var(--slotted-listbox-rows, 7);
94
+ --_columns: var(--slotted-listbox-columns, 1);
95
+
96
+ /**
97
+ * The cap. Measure belongs to `container`, so the family offers no axis for
98
+ * it and still has to stop a `max-content` surface from setting one option
99
+ * the width of the page.
100
+ */
101
+ --_measure: var(--slotted-listbox-measure, 24rem);
102
+
103
+ box-sizing: border-box;
104
+ color: var(--_text);
105
+ display: grid;
106
+ font-family: var(--slotted-font-family-sans);
107
+ font-size: var(--_font-size);
108
+ grid-auto-rows: min-content;
109
+ grid-template-columns: auto minmax(0, 1fr) auto;
110
+ inline-size: 100%;
111
+ max-inline-size: var(--_measure);
112
+ /**
113
+ * The block cap is stated rather than the height: a listbox of two options
114
+ * is two options tall, and one of two hundred stops at `rows` and scrolls.
115
+ * The inset is added so the last option is not cut in half by the padding
116
+ * the surface reserved for itself.
117
+ */
118
+ max-block-size: calc(var(--_rows) * var(--_option-height) + 2 * var(--_inset-block));
119
+ outline: none;
120
+ overflow-y: auto;
121
+ overscroll-behavior: contain;
122
+ padding-block: var(--_inset-block);
123
+ padding-inline: var(--_inset-inline);
124
+ /* The active option is scrolled to by script, and the browser must not
125
+ animate a second time over the top of it. */
126
+ scroll-behavior: auto;
127
+ }
128
+
129
+ [data-ui='listbox'][data-size='sm'] {
130
+ --_option-inset: var(--slotted-control-padding-inline-sm, 0.625rem);
131
+ --_option-height: var(--slotted-control-height-sm, 30px);
132
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
133
+ --_secondary-font-size: var(--slotted-font-size-xs, 0.75rem);
134
+ --_icon-size: var(--slotted-control-icon-size-sm, 14px);
135
+ --_column-gap: var(--slotted-control-gap-sm, 0.375rem);
136
+ }
137
+
138
+ [data-ui='listbox'][data-size='lg'] {
139
+ --_option-inset: var(--slotted-control-padding-inline-lg, 1rem);
140
+ --_option-height: var(--slotted-control-height-lg, 38px);
141
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
142
+ --_secondary-font-size: var(--slotted-control-font-size-sm, 12px);
143
+ --_icon-size: var(--slotted-control-icon-size-lg, 18px);
144
+ --_column-gap: var(--slotted-control-gap-lg, 0.5rem);
145
+ }
146
+
147
+ /* ── The two variants ─────────────────────────────────────────────────── */
148
+
149
+ /**
150
+ * `framed` is the standalone control: a box with an edge, which is what
151
+ * `<select multiple>` looks like and what a listbox presented on its own has
152
+ * to look like to be recognised as a control at all. `plain` is what `select`
153
+ * and `combobox` render inside their own layer, where the layer already has
154
+ * the edge and a second one would draw a box inside a box.
155
+ */
156
+ [data-ui='listbox'][data-variant='framed'] {
157
+ background: var(--_surface);
158
+ border: var(--_border-width) solid var(--_border-color);
159
+ border-radius: var(--_radius);
160
+ box-shadow: var(--_shadow);
161
+ }
162
+
163
+ [data-ui='listbox'][data-variant='plain'] {
164
+ background: transparent;
165
+ border: 0;
166
+ }
167
+
168
+ /**
169
+ * The ring goes on the box only while the box itself is the focused element,
170
+ * which is the `activedescendant` model. Under `roving` the ring belongs to
171
+ * the option, and a second ring around the whole listbox would say the
172
+ * listbox is the thing being operated.
173
+ */
174
+ [data-ui='listbox'][data-focus-mode='activedescendant']:focus-visible {
175
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, #2563eb);
176
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
177
+ }
178
+
179
+ [data-ui='listbox'][data-focus-mode='roving']:focus {
180
+ outline: none;
181
+ }
182
+
183
+ [data-ui='listbox'][data-invalid] {
184
+ --_border-color: var(--slotted-tone-danger-border);
185
+ --slotted-focus-ring-color: var(--slotted-focus-ring-color-invalid);
186
+ }
187
+
188
+ /* Width as well as colour (WCAG 1.4.1): in `high-contrast` the danger border
189
+ is the resting edge's black. An edge inside the border, so nothing moves,
190
+ in place of the resting lift — the one `text-field` draws. Disabled keeps
191
+ its own edge, as it keeps its own border colour. */
192
+ [data-ui='listbox'][data-invalid]:not([data-disabled]) {
193
+ --_invalid-edge-width: var(--slotted-control-invalid-edge-width, 1px);
194
+ --_shadow: inset 0 0 0 var(--_invalid-edge-width) var(--slotted-tone-danger-border);
195
+ }
196
+
197
+ [data-ui='listbox'][data-disabled] {
198
+ --_surface: var(--slotted-disabled-background, #e2e8f0);
199
+ --_text: var(--slotted-disabled-foreground, #64748b);
200
+ --_border-color: var(--slotted-disabled-border, #cbd5e1);
201
+
202
+ cursor: not-allowed;
203
+ }
204
+
205
+ [data-ui='listbox'][data-loading] {
206
+ cursor: progress;
207
+ }
208
+
209
+ /* ── The two layouts that are not one column ──────────────────────────── */
210
+
211
+ /**
212
+ * `columns` options across. The root's tracks become the columns, so an
213
+ * option owns its own three and the subgrid stops at the row. React Aria
214
+ * reads the laid-out geometry to decide what "the option below" is, which is
215
+ * a layout read during a key press; here the number is stated, the arrows
216
+ * step by it, and the picture and the keyboard cannot disagree.
217
+ */
218
+ [data-ui='listbox'][data-layout='grid'] {
219
+ column-gap: var(--_column-gap);
220
+ grid-template-columns: repeat(var(--_columns), minmax(0, 1fr));
221
+ max-block-size: none;
222
+ }
223
+
224
+ [data-ui='listbox'][data-layout='grid'] [data-part='option'] {
225
+ grid-column: auto;
226
+ grid-template-columns: auto minmax(0, 1fr) auto;
227
+ }
228
+
229
+ [data-ui='listbox'][data-layout='grid'] :is([data-part='group'], [data-part='canvas']) {
230
+ grid-template-columns: repeat(var(--_columns), minmax(0, 1fr));
231
+ }
232
+
233
+ /**
234
+ * Along the inline axis. The block cap does not apply — a horizontal listbox
235
+ * is one row tall — and the scroll turns with it, so `dir="rtl"` reverses the
236
+ * whole thing without a rule of its own.
237
+ */
238
+ [data-ui='listbox'][data-orientation='horizontal'] {
239
+ display: flex;
240
+ flex-direction: row;
241
+ max-block-size: none;
242
+ overflow-x: auto;
243
+ overflow-y: hidden;
244
+ }
245
+
246
+ [data-ui='listbox'][data-orientation='horizontal'] [data-part='option'] {
247
+ flex: 0 0 auto;
248
+ grid-template-columns: auto auto auto;
249
+ }
250
+
251
+ /* ── The canvas, which exists only while the list is windowed ─────────── */
252
+
253
+ /**
254
+ * It carries the whole list's height so the scrollbar is the length of the
255
+ * data rather than of what is rendered, and it starts the rendered slice at
256
+ * the offset the engine returned. `padding-block-start` rather than a
257
+ * transform, so the options are still in flow and the subgrid survives:
258
+ * `box-sizing: border-box` makes the padding part of the stated height, and
259
+ * the rows below it overflow a box nothing clips.
260
+ *
261
+ * `role="presentation"` is what keeps the options owned by the listbox. A
262
+ * generic element between a `listbox` and its `option`s is an owned element
263
+ * with no permitted role, and the role is what makes it transparent.
264
+ */
265
+ [data-ui='listbox'] > [data-part='canvas'] {
266
+ /**
267
+ * `align-content: start` and `grid-auto-rows: min-content` together, and
268
+ * neither is optional: a grid with a definite block size and automatic rows
269
+ * stretches those rows to fill it, so eight options inside a canvas six
270
+ * thousand pixels tall became eight rows of eight hundred and fifty and the
271
+ * window rendered nothing anybody could see.
272
+ */
273
+ align-content: start;
274
+ block-size: var(--_canvas-size, auto);
275
+ box-sizing: border-box;
276
+ display: grid;
277
+ grid-auto-rows: min-content;
278
+ grid-column: 1 / -1;
279
+ grid-template-columns: subgrid;
280
+ padding-block-start: var(--_canvas-offset, 0);
281
+ position: relative;
282
+ }
283
+
284
+ /**
285
+ * A row the window does not cover and the family renders anyway: the active
286
+ * option, and the anchor a range is measured from. Without it the element
287
+ * `aria-activedescendant` names leaves the document under a momentum scroll,
288
+ * and the reader is operating a widget with no focused option at all.
289
+ *
290
+ * It sits at its own place on the canvas rather than being pulled into view,
291
+ * because the reader scrolled it out and moving it back would take the
292
+ * scrollbar away from them.
293
+ */
294
+ [data-ui='listbox'] [data-part='option'][data-pinned] {
295
+ /* Out of flow, so out of the subgrid: it states the three columns itself.
296
+ They are the option's own rather than the listbox's, which is the price
297
+ of rendering a row the window does not reach. */
298
+ grid-template-columns: auto minmax(0, 1fr) auto;
299
+ inset-inline: 0;
300
+ position: absolute;
301
+ }
302
+
303
+ /* ── Groups ───────────────────────────────────────────────────────────── */
304
+
305
+ [data-ui='listbox'] [data-part='group'] {
306
+ display: grid;
307
+ grid-column: 1 / -1;
308
+ grid-template-columns: subgrid;
309
+ }
310
+
311
+ [data-ui='listbox'] [data-part='group-label'] {
312
+ color: var(--_muted);
313
+ font-size: var(--_secondary-font-size);
314
+ font-weight: var(--slotted-font-weight-semibold, 600);
315
+ grid-column: 1 / -1;
316
+ line-height: var(--slotted-line-height-sm, 1.45);
317
+ padding-block: var(--slotted-space-2, 0.25rem);
318
+ padding-inline: var(--_option-inset);
319
+ }
320
+
321
+ /**
322
+ * The rule between two groups is the group's own edge rather than a separator
323
+ * element: ARIA 1.2 lets a `listbox` own `option` and `group` and nothing
324
+ * else, so an `hr` inside one is an owned element with no permitted role.
325
+ */
326
+ [data-ui='listbox'] [data-part='group'] + [data-part='group'] {
327
+ border-block-start: var(--_border-width) solid
328
+ var(--slotted-listbox-group-border-color, var(--slotted-border-subtle, #e2e8f0));
329
+ margin-block-start: var(--slotted-space-2, 0.25rem);
330
+ padding-block-start: var(--slotted-space-2, 0.25rem);
331
+ }
332
+
333
+ /* ── Options ──────────────────────────────────────────────────────────── */
334
+
335
+ [data-ui='listbox'] [data-part='option'] {
336
+ align-content: center;
337
+ background: transparent;
338
+ border-radius: var(--_option-radius);
339
+ box-sizing: border-box;
340
+ color: inherit;
341
+ cursor: default;
342
+ display: grid;
343
+ grid-column: 1 / -1;
344
+ grid-template-columns: subgrid;
345
+ /**
346
+ * The line box belongs to the option and every cell inherits it, so `1lh`
347
+ * on the mark resolves against the same line the label is set on. Stated
348
+ * anywhere else, a mark beside a label that wrapped sits two pixels above
349
+ * the text it annotates.
350
+ */
351
+ line-height: var(--slotted-line-height-md, 1.5);
352
+ min-block-size: var(--_option-height);
353
+ padding-block: var(--slotted-space-2, 0.25rem);
354
+ padding-inline: var(--_option-inset);
355
+ position: relative;
356
+ /* The one gap inside an option, between a label and the line under it. */
357
+ row-gap: var(--slotted-space-1, 0.125rem);
358
+ text-align: start;
359
+ transition:
360
+ background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
361
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
362
+ color var(--slotted-control-transition-duration, var(--slotted-duration-short))
363
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
364
+ }
365
+
366
+ /**
367
+ * Hover is a pointer's own feedback and nothing more. Unlike `menu`, it does
368
+ * not move the active option: a listbox holds a selection as well as a
369
+ * highlight, and a hover that moved the highlight would fight the arrow keys
370
+ * a reader is holding down.
371
+ */
372
+ [data-ui='listbox']:not([data-disabled]) [data-part='option']:not([data-disabled]):hover {
373
+ background-color: var(--_highlight);
374
+ }
375
+
376
+ [data-ui='listbox']:not([data-disabled]) [data-part='option']:not([data-disabled]):active {
377
+ background-color: var(--_highlight-active);
378
+ }
379
+
380
+ /**
381
+ * Where the keyboard is, and only while the keyboard is somewhere it can be.
382
+ *
383
+ * Under `roving` the ground is drawn while focus is inside the listbox: an
384
+ * option nobody is on is not the active one, and painting it anyway made a
385
+ * hovered option and the active option the same picture. Under
386
+ * `activedescendant` it is drawn unconditionally, because focus may
387
+ * legitimately be elsewhere — a `combobox` keeps it in its own text field and
388
+ * this ground is then the only thing that says which option is live.
389
+ */
390
+ [data-ui='listbox'][data-focus-mode='roving']:focus-within [data-part='option'][data-current],
391
+ [data-ui='listbox'][data-focus-mode='activedescendant'] [data-part='option'][data-current] {
392
+ background-color: var(--_highlight);
393
+ }
394
+
395
+ /**
396
+ * The ring, and it is drawn from the listbox rather than from the option.
397
+ * `:focus-within` is what makes one rule cover both focus models: under
398
+ * `roving` the focus is on the option and under `activedescendant` it is on
399
+ * the root, and in both the answer to "is the keyboard in this listbox" is
400
+ * the same. Keying on the option's own `:focus-visible` would have drawn
401
+ * nothing at all in the model where the option never holds focus.
402
+ *
403
+ * An inset `outline` rather than a `::after` border: an outline follows the
404
+ * option's own radius without a second radius to keep in step, and forced
405
+ * colours replace its paint on their own, where a pseudo-element's border
406
+ * would be decoration the theme cannot reach.
407
+ */
408
+ [data-ui='listbox']:focus-within [data-part='option'][data-current] {
409
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, #2563eb);
410
+ outline-offset: calc(-1 * var(--slotted-focus-ring-width, 2px));
411
+ }
412
+
413
+ [data-ui='listbox'] [data-part='option']:focus {
414
+ outline: none;
415
+ }
416
+
417
+ /**
418
+ * What the reader chose. The ground says it at a glance, the ink says it
419
+ * again, and the mark says it where neither can: in the `high-contrast`
420
+ * scheme the accent surface *is* the plain surface, and every tone surface
421
+ * becomes `Canvas` under forced colours, so a selection painted with a fill
422
+ * alone disappears in the mode that needs it most — the same failure WCAG
423
+ * 2.2 SC 1.4.1 describes for colour on its own. A bar on the start edge said
424
+ * it once; a rail for state is out of the system (R1), and the `indicator`
425
+ * below is the mark that carries it instead.
426
+ */
427
+ [data-ui='listbox'] [data-part='option'][data-selected] {
428
+ background-color: var(--_selected-surface);
429
+ color: var(--_selected-text);
430
+ }
431
+
432
+ /**
433
+ * Two chosen neighbours are one block (LB2). Every option keeps its own
434
+ * radius, so two selected siblings painted with the same ground met at a
435
+ * waist — four rounded corners inside the block — and a multiple selection
436
+ * read as a column of separate chips. The second gives up its top corners
437
+ * and the first its bottom ones, so the block is rounded only where the
438
+ * selection begins and ends. `select` and `combobox` draw the same ground
439
+ * and answer the same way; `selection-block.test.mjs` holds the three to it.
440
+ */
441
+ [data-ui='listbox'] [data-part='option'][data-selected] + [data-part='option'][data-selected] {
442
+ border-start-end-radius: 0;
443
+ border-start-start-radius: 0;
444
+ }
445
+
446
+ [data-ui='listbox']
447
+ [data-part='option'][data-selected]:has(+ [data-part='option'][data-selected]) {
448
+ border-end-end-radius: 0;
449
+ border-end-start-radius: 0;
450
+ }
451
+
452
+ [data-ui='listbox']:not([data-disabled])
453
+ [data-part='option'][data-selected]:not([data-disabled]):hover {
454
+ background-color: var(--slotted-tone-accent-subtle-hover);
455
+ }
456
+
457
+ [data-ui='listbox']:not([data-disabled])
458
+ [data-part='option'][data-selected]:not([data-disabled]):active {
459
+ background-color: var(--slotted-tone-accent-subtle-active);
460
+ }
461
+
462
+ /**
463
+ * A disabled option keeps its place in the set and stays reachable when
464
+ * `navigateDisabled` says so, which the pattern allows and a native disabled
465
+ * control cannot do. It therefore has to answer the highlight with something,
466
+ * and answer it more quietly than an option that would do anything.
467
+ */
468
+ [data-ui='listbox'] [data-part='option'][data-disabled] {
469
+ color: var(--slotted-disabled-foreground, #64748b);
470
+ cursor: not-allowed;
471
+ }
472
+
473
+ /**
474
+ * Disabled outranks selected, which is the order `STATE_PRECEDENCE` declares.
475
+ * The mark stays drawn — the reader still needs to know which option was
476
+ * chosen — and the ground stops claiming the option is theirs to change.
477
+ */
478
+ [data-ui='listbox'] [data-part='option'][data-disabled]:is([data-selected], [data-current]) {
479
+ background-color: var(--slotted-disabled-background, #e2e8f0);
480
+ color: var(--slotted-disabled-foreground, #64748b);
481
+ }
482
+
483
+ [data-ui='listbox'][data-readonly] [data-part='option'] {
484
+ cursor: default;
485
+ }
486
+
487
+ /* ── The cells ────────────────────────────────────────────────────────── */
488
+
489
+ /**
490
+ * Column one, and the reason a chosen option and an unchosen one line their
491
+ * labels up: the mark's box exists whether or not the mark is drawn, so a
492
+ * list of options does not shift by sixteen pixels as one is chosen.
493
+ */
494
+ [data-ui='listbox'] :is([data-part='indicator'], [data-part='icon']) {
495
+ /* One line box tall, so a mark beside a label that wrapped onto three lines
496
+ sits on the first of them rather than in the middle of the option. */
497
+ block-size: 1lh;
498
+ grid-column: 1;
499
+ grid-row: 1;
500
+ inline-size: var(--_icon-size);
501
+ justify-self: center;
502
+ margin-inline-end: var(--_column-gap);
503
+ }
504
+
505
+ /**
506
+ * The tick is the library's own `Glyph`, an `<svg>` the binding draws inside
507
+ * an `icon` box, sized here to the column so the mark and a consumer's icon
508
+ * in the same column are one size.
509
+ */
510
+ [data-ui='listbox'] [data-part='indicator'] {
511
+ --slotted-icon-size: var(--_icon-size);
512
+
513
+ align-items: center;
514
+ display: flex;
515
+ justify-content: center;
516
+ opacity: 0;
517
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-standard);
518
+ }
519
+
520
+ [data-ui='listbox'] [data-part='option'][data-selected] > [data-part='indicator'] {
521
+ opacity: 1;
522
+ }
523
+
524
+ /**
525
+ * The option's own artwork, in the mark column. The box holds content and
526
+ * paints none: a `Glyph` in an `Icon` box, or an element of the consumer's
527
+ * own, centred and sized from the column, as the tick is.
528
+ */
529
+ [data-ui='listbox'] [data-part='icon'] {
530
+ --slotted-icon-size: var(--_icon-size);
531
+
532
+ align-items: center;
533
+ display: flex;
534
+ justify-content: center;
535
+ }
536
+
537
+ /* Column two, and the only column that gives way. */
538
+ [data-ui='listbox'] [data-part='label'] {
539
+ grid-column: 2;
540
+ grid-row: 1;
541
+ min-inline-size: 0;
542
+ overflow-wrap: anywhere;
543
+ }
544
+
545
+ [data-ui='listbox'] [data-part='description'] {
546
+ color: var(--_muted);
547
+ font-size: var(--_secondary-font-size);
548
+ grid-column: 2;
549
+ grid-row: 2;
550
+ line-height: var(--slotted-line-height-sm, 1.45);
551
+ min-inline-size: 0;
552
+ overflow-wrap: anywhere;
553
+ }
554
+
555
+ [data-ui='listbox']
556
+ [data-part='option']:is([data-disabled], [data-selected])
557
+ > [data-part='description'] {
558
+ color: inherit;
559
+ }
560
+
561
+ /**
562
+ * Column three: the value beside the name — a count, a price, a status word.
563
+ * `menu` calls the same column `shortcut` because a menu's third column holds
564
+ * a key binding; naming it that here would be a lie on every listbox in the
565
+ * library. Never narrower than its own content, so a long label wraps in
566
+ * column two rather than pushing the value off the surface.
567
+ */
568
+ [data-ui='listbox'] [data-part='meta'] {
569
+ color: var(--_muted);
570
+ font-variant-numeric: tabular-nums;
571
+ grid-column: 3;
572
+ grid-row: 1;
573
+ justify-self: end;
574
+ margin-inline-start: var(--slotted-space-6, 1rem);
575
+ white-space: nowrap;
576
+ }
577
+
578
+ [data-ui='listbox']
579
+ [data-part='option']:is([data-disabled], [data-selected])
580
+ > [data-part='meta'] {
581
+ color: inherit;
582
+ }
583
+
584
+ /* ── Empty, and the row that asks for more ────────────────────────────── */
585
+
586
+ /**
587
+ * Both keep the surface's own measure rather than collapsing to a two-pixel
588
+ * box, which is what a consumer sees flash when the state is not painted at
589
+ * all. The message lives inside the listbox: outside it the reader hears
590
+ * "listbox, 0 items" and then nothing.
591
+ */
592
+ [data-ui='listbox'] :is([data-part='empty'], [data-part='loader']) {
593
+ align-content: center;
594
+ color: var(--_muted);
595
+ display: grid;
596
+ grid-column: 1 / -1;
597
+ min-block-size: var(--_option-height);
598
+ padding-block: var(--slotted-space-5, 0.75rem);
599
+ padding-inline: var(--_option-inset);
600
+ text-align: center;
601
+ }
602
+
603
+ /**
604
+ * The row at the end of a page that has not arrived. It is an `option` by
605
+ * role — a listbox owns nothing else — so it carries the same geometry and
606
+ * says what it is with a pulse rather than with a second element.
607
+ */
608
+ [data-ui='listbox'] [data-part='loader'] {
609
+ animation: slotted-listbox-loader var(--slotted-duration-loop) ease-in-out infinite;
610
+ }
611
+
612
+ @keyframes slotted-listbox-loader {
613
+ 0%,
614
+ 100% {
615
+ opacity: 0.45;
616
+ }
617
+
618
+ 50% {
619
+ opacity: 1;
620
+ }
621
+ }
622
+
623
+ /**
624
+ * An animation that repeats forever has to stop rather than hurry: shortening
625
+ * it is insufficient, which is why the loop durations do not collapse with
626
+ * the rest of the motion scale.
627
+ */
628
+ @media (prefers-reduced-motion: reduce) {
629
+ [data-ui='listbox'] [data-part='loader'] {
630
+ animation: none;
631
+ opacity: 0.7;
632
+ }
633
+ }
634
+
635
+ /* ── Forced colours ───────────────────────────────────────────────────── */
636
+
637
+ /**
638
+ * The two things the theme cannot reach. Every tone surface becomes `Canvas`
639
+ * under forced colours, so both the selection and the highlight — which are
640
+ * this family's whole state vocabulary, not decoration — would disappear and
641
+ * leave a reader unable to tell what they have chosen from what they are on.
642
+ */
643
+ @media (forced-colors: active) {
644
+ [data-ui='listbox'] [data-part='option'][data-selected] {
645
+ background-color: Highlight;
646
+ color: HighlightText;
647
+ forced-color-adjust: none;
648
+ }
649
+
650
+ [data-ui='listbox'] [data-part='option'][data-disabled] {
651
+ color: GrayText;
652
+ }
653
+ }
654
+
655
+ /*
656
+ * Forced colours remove every box shadow, and the edge with them. The width
657
+ * comes back as an outline inside the border, and where focus draws the ring
658
+ * on the same box an invalid ring is double, which a valid one is not.
659
+ */
660
+ @media (forced-colors: active) {
661
+ [data-ui='listbox'][data-variant='framed'][data-invalid]:not([data-disabled]) {
662
+ outline: var(--_invalid-edge-width) solid CanvasText;
663
+ outline-offset: calc(-1 * (var(--_border-width) + var(--_invalid-edge-width)));
664
+ }
665
+
666
+ [data-ui='listbox'][data-variant='framed'][data-invalid]:not(
667
+ [data-disabled]
668
+ )[data-focus-mode='activedescendant']:focus-visible {
669
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
670
+ var(--slotted-focus-ring-color, Highlight);
671
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
672
+ }
673
+ }
674
+ }