@sableui/react 0.13.2 → 0.14.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 (182) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +114 -0
  3. package/CONTRIBUTING.md +6 -4
  4. package/DESIGN.md +8 -3
  5. package/dist/blocks/approval-block.js +93 -0
  6. package/dist/blocks/approval-block.meta.js +23 -0
  7. package/dist/blocks/index.d.ts +16 -1
  8. package/dist/blocks/index.js +4 -0
  9. package/dist/components/accordion/accordion.meta.js +8 -2
  10. package/dist/components/area-chart/area-chart.meta.js +10 -3
  11. package/dist/components/attachment/attachment.meta.js +26 -0
  12. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  13. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  14. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  15. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  16. package/dist/components/button/button.js +21 -4
  17. package/dist/components/button/button.meta.js +7 -7
  18. package/dist/components/calendar/calendar.js +18 -1
  19. package/dist/components/calendar/calendar.meta.js +38 -0
  20. package/dist/components/carousel/carousel.js +6 -4
  21. package/dist/components/carousel/carousel.meta.js +42 -0
  22. package/dist/components/chat-composer/chat-composer.js +21 -21
  23. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  24. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  25. package/dist/components/checkbox/checkbox-mark.js +8 -1
  26. package/dist/components/checkbox/checkbox.js +3 -1
  27. package/dist/components/checkbox/checkbox.meta.js +32 -0
  28. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  29. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  30. package/dist/components/code-block/code-block.js +14 -3
  31. package/dist/components/code-block/code-block.meta.js +28 -1
  32. package/dist/components/collapsible/collapsible.meta.js +32 -0
  33. package/dist/components/color-field/color-field.meta.js +26 -0
  34. package/dist/components/combobox/combobox.meta.js +44 -0
  35. package/dist/components/command-menu/command-menu.js +8 -3
  36. package/dist/components/command-menu/command-menu.meta.js +38 -0
  37. package/dist/components/confirm-button/confirm-button.js +179 -0
  38. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  39. package/dist/components/confirm-button/index.js +7 -0
  40. package/dist/components/data-table/data-table.js +6 -2
  41. package/dist/components/data-table/data-table.meta.js +32 -0
  42. package/dist/components/entity/entity.meta.js +26 -0
  43. package/dist/components/feedback/feedback.js +113 -47
  44. package/dist/components/feedback/feedback.meta.js +49 -4
  45. package/dist/components/input-group/input-group.meta.js +27 -1
  46. package/dist/components/line-chart/line-chart.meta.js +10 -3
  47. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  48. package/dist/components/note/note.meta.js +32 -0
  49. package/dist/components/notification-badge/notification-badge.js +17 -5
  50. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  51. package/dist/components/number-ticker/index.js +7 -0
  52. package/dist/components/number-ticker/number-ticker.js +246 -0
  53. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  54. package/dist/components/otp-field/otp-field.js +23 -3
  55. package/dist/components/otp-field/otp-field.meta.js +23 -2
  56. package/dist/components/panel-group/panel-group.meta.js +26 -0
  57. package/dist/components/panel-section/panel-section.js +37 -10
  58. package/dist/components/panel-section/panel-section.meta.js +32 -0
  59. package/dist/components/pie-chart/pie-chart.js +20 -11
  60. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  61. package/dist/components/pill/pill.meta.js +32 -0
  62. package/dist/components/radar-chart/radar-chart.js +4 -3
  63. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  64. package/dist/components/radial-chart/radial-chart.js +22 -22
  65. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  66. package/dist/components/radio/radio.meta.js +26 -0
  67. package/dist/components/radio-card/radio-card.meta.js +38 -0
  68. package/dist/components/relative-time/relative-time.meta.js +32 -0
  69. package/dist/components/scroller/scroller.meta.js +32 -0
  70. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  71. package/dist/components/search-input/search-input.js +9 -7
  72. package/dist/components/search-input/search-input.meta.js +26 -0
  73. package/dist/components/show-more/show-more.js +1 -4
  74. package/dist/components/show-more/show-more.meta.js +38 -0
  75. package/dist/components/side-nav/side-nav.js +21 -1
  76. package/dist/components/side-nav/side-nav.meta.js +32 -0
  77. package/dist/components/side-nav/side-nav.variants.js +3 -1
  78. package/dist/components/sidebar/sidebar.js +31 -8
  79. package/dist/components/sidebar/sidebar.meta.js +38 -0
  80. package/dist/components/slider/slider.meta.js +26 -0
  81. package/dist/components/sparkline/sparkline.js +7 -4
  82. package/dist/components/sparkline/sparkline.meta.js +9 -2
  83. package/dist/components/table/table.meta.js +26 -0
  84. package/dist/components/tabs/tabs.js +27 -8
  85. package/dist/components/tabs/tabs.meta.js +46 -3
  86. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  87. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  88. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  89. package/dist/components/toggle/toggle.js +28 -5
  90. package/dist/components/toggle/toggle.meta.js +44 -2
  91. package/dist/components/tool-call/index.js +8 -0
  92. package/dist/components/tool-call/tool-call.js +117 -0
  93. package/dist/components/tool-call/tool-call.meta.js +147 -0
  94. package/dist/components/tool-call/tool-call.variants.js +33 -0
  95. package/dist/components/tooltip/index.js +3 -1
  96. package/dist/components/tooltip/tooltip.js +84 -0
  97. package/dist/components/tooltip/tooltip.meta.js +16 -1
  98. package/dist/components/tree/tree.js +71 -6
  99. package/dist/components/tree/tree.meta.js +32 -0
  100. package/dist/components/video/video.meta.js +32 -0
  101. package/dist/file-hashes.json +114 -97
  102. package/dist/foundations/motion.js +49 -3
  103. package/dist/index.d.ts +614 -44
  104. package/dist/index.js +4 -1
  105. package/dist/lib/chart/cartesian.js +9 -10
  106. package/dist/lib/chart/chart-center.js +30 -0
  107. package/dist/lib/chart/chart-frame.js +15 -8
  108. package/dist/lib/chart/dim.js +13 -0
  109. package/dist/lib/chart/format.js +7 -1
  110. package/dist/lib/chart/motion.js +6 -4
  111. package/dist/lib/highlight-glide.js +68 -10
  112. package/dist/lib/taxonomy.js +3 -0
  113. package/dist/motion/MotionProvider.js +45 -3
  114. package/dist/motion/Reveal.js +1 -1
  115. package/dist/motion/TextReveal.js +1 -1
  116. package/dist/motion/index.js +15 -2
  117. package/dist/motion/useAutoSize.js +42 -0
  118. package/dist/motion/useShake.js +25 -0
  119. package/dist/styles.css +298 -5
  120. package/llms/blocks/approval-block.md +128 -0
  121. package/llms/components/accordion.md +3 -2
  122. package/llms/components/area-chart.md +4 -3
  123. package/llms/components/attachment.md +7 -0
  124. package/llms/components/bar-chart.md +5 -4
  125. package/llms/components/bottom-nav.md +8 -0
  126. package/llms/components/breadcrumbs.md +7 -0
  127. package/llms/components/button.md +6 -6
  128. package/llms/components/calendar.md +9 -0
  129. package/llms/components/carousel.md +17 -0
  130. package/llms/components/chat-composer.md +9 -0
  131. package/llms/components/checkbox-card.md +8 -0
  132. package/llms/components/checkbox.md +8 -0
  133. package/llms/components/code-block.md +8 -0
  134. package/llms/components/collapsible.md +8 -0
  135. package/llms/components/color-field.md +7 -0
  136. package/llms/components/combobox.md +10 -0
  137. package/llms/components/command-menu.md +9 -0
  138. package/llms/components/confirm-button.md +92 -0
  139. package/llms/components/data-table.md +8 -0
  140. package/llms/components/entity.md +7 -0
  141. package/llms/components/feedback.md +22 -3
  142. package/llms/components/input-group.md +8 -1
  143. package/llms/components/line-chart.md +4 -3
  144. package/llms/components/middle-truncate.md +1 -1
  145. package/llms/components/note.md +8 -0
  146. package/llms/components/notification-badge.md +10 -2
  147. package/llms/components/number-ticker.md +86 -0
  148. package/llms/components/otp-field.md +15 -1
  149. package/llms/components/panel-group.md +7 -0
  150. package/llms/components/panel-section.md +8 -0
  151. package/llms/components/pie-chart.md +6 -4
  152. package/llms/components/pill.md +8 -0
  153. package/llms/components/radar-chart.md +4 -3
  154. package/llms/components/radial-chart.md +6 -4
  155. package/llms/components/radio-card.md +9 -0
  156. package/llms/components/radio.md +7 -0
  157. package/llms/components/relative-time.md +8 -0
  158. package/llms/components/scroller.md +8 -0
  159. package/llms/components/scrub-field.md +8 -0
  160. package/llms/components/search-input.md +7 -0
  161. package/llms/components/show-more.md +9 -0
  162. package/llms/components/side-nav.md +8 -0
  163. package/llms/components/sidebar.md +9 -0
  164. package/llms/components/slider.md +7 -0
  165. package/llms/components/sparkline.md +3 -2
  166. package/llms/components/table.md +7 -0
  167. package/llms/components/tabs.md +20 -2
  168. package/llms/components/theme-selector.md +8 -0
  169. package/llms/components/theme-switcher.md +17 -0
  170. package/llms/components/toggle.md +19 -2
  171. package/llms/components/tool-call.md +115 -0
  172. package/llms/components/tooltip.md +11 -1
  173. package/llms/components/tree.md +8 -0
  174. package/llms/components/video.md +8 -0
  175. package/llms/foundations/motion.md +48 -3
  176. package/llms/reference/utilities.md +23 -1
  177. package/llms/tasks/reviewing-motion.md +14 -0
  178. package/llms-full.txt +955 -49
  179. package/llms.txt +15 -10
  180. package/package.json +1 -1
  181. package/registry.json +2565 -210
  182. package/targets/ios.json +27 -0
@@ -15,6 +15,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
15
15
  - Do: Set `length` to the exact length the server sends, and verify in `onValueComplete` (or with `autoSubmit` inside a form). Most users paste or autofill the whole code at once, so the moment of completion is the natural submit.
16
16
  - Do: Label it through a `Field` ("Verification code") and say where the code was sent in the description. The first slot takes the field's name; the rest are announced by position.
17
17
  - Do: Use `groupSize` for codes of 6 or more so the user can check the code in chunks against the message they are reading from.
18
+ - Do: Clear the error as soon as the user edits the code (`onValueChange` → reset the error), and set it again when the next code is refused. Each refusal is then a fresh change to invalid, so the field shakes for every wrong code, not only the first.
18
19
  - Don't: Do not use OtpField for a password, an arbitrary-length PIN, or anything the user types from memory. Slots promise a fixed length; use Input with `type="password"` and `autocomplete="current-password"` instead.
19
20
  - Don't: Do not intercept paste or clear the field on a wrong code before the user has seen the error. Blocking paste breaks SMS autofill and password managers, and wiping the slots erases the evidence of which digit was wrong.
20
21
  - Don't: Do not swap it for NumberField because the code is digits. A code is an identifier, not a quantity: NumberField strips leading zeros and offers steppers that make no sense here.
@@ -41,7 +42,8 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
41
42
  | autoSubmit | boolean | false | Submit the owning form when the code becomes complete. |
42
43
  | mask | boolean | false | Render slots as password inputs. Only for codes that are secret on screen, such as a card PIN. |
43
44
  | getSlotLabel | (index: number, length: number) => string | Character N of L | Accessible name for slots after the first. Override to localize. |
44
- | aria-invalid | boolean | — | Marks every slot invalid (destructive border). `<Field error>` sets it for you. |
45
+ | aria-invalid | boolean | — | Marks every slot invalid (destructive border and digits). Turning it on after mount shakes the field once. `<Field error>` sets it for you. |
46
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The shake is the flourish; the destructive colour stays at every level. |
45
47
  | disabled / readOnly / required / name / id / form | — | — | Standard form-control props. `id` lands on the first slot, so `<Field>` and `<label htmlFor>` name the field. |
46
48
 
47
49
  ## Tokens used
@@ -53,6 +55,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
53
55
  - **arrival** · Root · _no motion_ — A code field is part of the form it sits in and appears with it. Animating it in would delay the one thing the user arrived to do — paste the code they just copied.
54
56
  - **response** · Slot · `motion-interactive` — Focus moves between slots on every keystroke. The eased ring and border are what let the eye follow the cursor across six identical boxes instead of hunting for it after each digit.
55
57
  - **transition** · Slot · `motion-interactive` — A filled slot strengthens its border, so a pasted code visibly lands in every slot at once. Easing it reads as the code arriving rather than the field flickering.
58
+ - **transition** · Root · `motion-shake` — A refused code shakes the field once as it turns invalid — the short horizontal no that makes a second wrong code feel noticed rather than ignored. It never carries the refusal alone: the digits and borders turn destructive and the Field says why. It plays on the change to invalid, not on mount, and is expressive, so `subtle` keeps only the colour.
56
59
  - **activity** · Root · _no motion_ — Verifying the code is the form's work, not the field's. Show it on the submit Button's `loading` state, where the user expects the answer to appear.
57
60
 
58
61
  ## Accessibility
@@ -61,6 +64,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
61
64
  - Only one slot is in the tab order. Arrow keys, Home/End and Backspace move between slots; Tab leaves the whole field.
62
65
  - The first slot carries autocomplete="one-time-code" and the numeric virtual keyboard, so iOS and Android offer the SMS code above the keyboard.
63
66
  - Slots render at 20px (`text-mono-lg`), above the 16px floor that stops iOS Safari zooming on focus.
67
+ - An invalid code is never signalled by motion alone: every slot carries aria-invalid, the digits and borders turn destructive, and the Field's error message is wired through aria-describedby. The shake resolves instantly under reduced motion and `subtle`.
64
68
 
65
69
  ## Related
66
70
 
@@ -92,6 +96,16 @@ import { Field, OtpField } from "@sableui/react"
92
96
  <Field label="Verification code" error="That code has expired. Request a new code and try again."><OtpField length={6} groupSize={3} defaultValue="482913" /></Field>
93
97
  ```
94
98
 
99
+ ### Verify on completion, shake on a wrong code
100
+
101
+ ```tsx
102
+ import { Field, OtpField } from "@sableui/react"
103
+
104
+ const [code, setCode] = useState("")
105
+ const [wrong, setWrong] = useState(false)
106
+ <Field label="Verification code" error={wrong ? "That code does not match. Check the message and try again." : undefined}><OtpField length={6} groupSize={3} value={code} onValueChange={(next) => { setCode(next); setWrong(false) }} onValueComplete={(value) => setWrong(value !== expected)} /></Field>
107
+ ```
108
+
95
109
  ### Alphanumeric
96
110
 
97
111
  ```tsx
@@ -39,6 +39,13 @@ Use PanelGroup for the body of a tool workspace: fixed-width Panels docked left
39
39
 
40
40
  `primary`, `panel`, `hairline`, `hairline-strong`
41
41
 
42
+ ## Motion
43
+
44
+ - **arrival** · Root · _no motion_ — Panels are the app's frame, present from first paint. A dock that slid in on load would make the workspace look like it is still assembling.
45
+ - **response** · PanelResizer · `motion-interactive` — The hairline widens into a visible bar on hover and turns primary on keyboard focus, eased, so a 1px edge says it can be grabbed before the pointer commits.
46
+ - **transition** · Panel · _no motion_ — Width tracks the pointer one-to-one while dragging, and keyboard steps land at once. An eased width would trail the cursor, and resizing is a layout change: easing it would reflow the canvas beside the panel on every frame.
47
+ - **activity** · Root · _no motion_ — Layout chrome has no working state.
48
+
42
49
  ## Accessibility
43
50
 
44
51
  - PanelResizer is a role=separator with aria-orientation, aria-valuenow/min/max, and is focusable — a pointer-only resizer is a keyboard trap.
@@ -48,6 +48,14 @@ Use PanelSection to group properties inside an inspector or docked panel, with P
48
48
 
49
49
  `foreground`, `muted-foreground`, `hairline`
50
50
 
51
+ ## Motion
52
+
53
+ - **arrival** · Root · _no motion_ — Sections are part of the inspector they sit in and arrive with it. Animating each in would make a panel switch look like a page load.
54
+ - **response** · PropertyGroup trigger · `motion-interactive` — The fold row eases its focus ring, so arrowing down a dense inspector shows where the keyboard is without the ring flickering from row to row.
55
+ - **transition** · PropertyGroup panel · `motion-disclosure` — The group opens by easing its height, so the rows below slide down to make room instead of jumping. In an inspector, the next section moving is otherwise how you lose your place.
56
+ - **transition** · PropertyGroup indicator · `motion-icon-swap` — The + collapses into − as the group opens, both glyphs stacked as one mark, so the indicator reads as this row changing state rather than a different icon landing.
57
+ - **activity** · Root · _no motion_ — The inspector edits values in place; nothing in it loads.
58
+
51
59
  ## Accessibility
52
60
 
53
61
  - PropertyGroup's row is a real button with aria-expanded (Collapsible); its summary is part of the button's name, so a closed group still announces its value.
@@ -36,7 +36,8 @@ Use PieChart for a share of one total across two to six categories — traffic s
36
36
  | tooltip | boolean \| { indicator?: "dot" \| "line" \| "dashed" \| "none"; hideLabel?: boolean; labelFormatter?: (label, datum) => ReactNode; showTotal?: boolean } | true | Show a tooltip on hover and keyboard focus, or configure it: the row marker, hiding the heading, rewriting it, adding a Total row. |
37
37
  | valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
38
38
  | locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
39
- | animate | boolean | true | Sweep the slices in clockwise when the chart scrolls into view. |
39
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The sweep, the slice morph and the rolling centre number are the flourish; the hover pull-out and dim are the feedback `subtle` keeps. |
40
+ | animate | boolean | true | Sweep the slices in clockwise when the chart scrolls into view. `animate={false}` is `motion="subtle"`. |
40
41
  | activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
41
42
  | defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
42
43
  | onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
@@ -47,9 +48,10 @@ Use PieChart for a share of one total across two to six categories — traffic s
47
48
 
48
49
  ## Motion
49
50
 
50
- - **arrival** · Slices · _custom (outside the recipe layer)_ — Once on screen the slices are revealed by a mask that sweeps clockwise from 12 o'clock — the whole is read as it fills. On the motion tokens; reduced motion shows the finished chart.
51
- - **response** · Active slice · _no motion_ — The hovered slice pulls outward and the rest dim at once; the geometry change itself eases (see transition).
52
- - **transition** · Slices · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place.
51
+ - **arrival** · Slices · _custom (outside the recipe layer)_ — Once on screen the slices are revealed by a mask that sweeps clockwise from 12 o'clock — the whole is read as it fills. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.
52
+ - **response** · Active slice · _no motion_ — The hovered slice pulls outward and the rest dim at once (mixed toward the surface, not faded); the geometry change itself eases (see transition). Essential: `subtle` keeps it.
53
+ - **transition** · Centre value · _custom (outside the recipe layer)_ — A numeric centre value rolls to its new total digit by digit (NumberTicker) when the data changes, so the headline is seen moving with the slices instead of being found different afterwards. Expressive: under `subtle` it updates in place.
54
+ - **transition** · Slices · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place.
53
55
  - **activity** · PieChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
54
56
 
55
57
  ## Accessibility
@@ -39,6 +39,14 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
39
39
 
40
40
  `palette-wash`, `palette-wash-strong`, `palette-edge`, `palette-solid`, `palette-text`, `palette-solid-ink`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `primary`, `primary-foreground`, `card`, `border`, `muted-foreground`, `foreground`
41
41
 
42
+ ## Motion
43
+
44
+ - **arrival** · Root · _no motion_ — Pills arrive with the filter row or the content they annotate. A chip that animated in alone would read as a filter someone just added.
45
+ - **response** · Root · `motion-interactive` — The hover wash eases, so sweeping the pointer along a filter row reads as one cursor moving rather than chips flashing in turn.
46
+ - **response** · Root · `motion-press` — A pill often toggles a filter that takes a moment to reach the results; the settle on press confirms the click landed before they change.
47
+ - **transition** · Root · `motion-interactive` — `selected` lifts the chip onto its selected surface on the same ease, so toggling reads as this chip changing state, not a different chip appearing in its place.
48
+ - **activity** · Root · _no motion_ — A chip is a choice, not a process. If applying it is slow, the results show it.
49
+
42
50
  ## Accessibility
43
51
 
44
52
  - Renders a native <button type="button"> for focus and keyboard activation.
@@ -35,7 +35,8 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
35
35
  | tooltip | boolean \| { indicator?: "dot" \| "line" \| "dashed" \| "none"; hideLabel?: boolean; labelFormatter?: (label, datum) => ReactNode; showTotal?: boolean } | true | Show a tooltip on hover and keyboard focus, or configure it: the row marker, hiding the heading, rewriting it, adding a Total row. |
36
36
  | valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
37
37
  | locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
38
- | animate | boolean | true | Grow the polygons from the centre when the chart scrolls into view. |
38
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The polygons growing in and morphing are the flourish; the hover highlight is the feedback `subtle` keeps. |
39
+ | animate | boolean | true | Grow the polygons from the centre when the chart scrolls into view. `animate={false}` is `motion="subtle"`. |
39
40
  | activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
40
41
  | defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
41
42
  | onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
@@ -46,9 +47,9 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
46
47
 
47
48
  ## Motion
48
49
 
49
- - **arrival** · Polygon · _custom (outside the recipe layer)_ — Once on screen each polygon grows from the centre and fades in, series staggered, on the emphasized easing. On the motion tokens; reduced motion shows the finished chart.
50
+ - **arrival** · Polygon · _custom (outside the recipe layer)_ — Once on screen each polygon grows from the centre and fades in, series staggered, on the emphasized easing. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.
50
51
  - **response** · Active spoke · _no motion_ — The hovered spoke's vertices and label highlight at once.
51
- - **transition** · Polygon · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place.
52
+ - **transition** · Polygon · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place.
52
53
  - **activity** · RadarChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
53
54
 
54
55
  ## Accessibility
@@ -37,7 +37,8 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
37
37
  | tooltip | boolean \| { indicator?: "dot" \| "line" \| "dashed" \| "none"; hideLabel?: boolean; labelFormatter?: (label, datum) => ReactNode; showTotal?: boolean } | true | Show a tooltip on hover and keyboard focus, or configure it: the row marker, hiding the heading, rewriting it, adding a Total row. |
38
38
  | valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
39
39
  | locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
40
- | animate | boolean | true | Draw the rings round from their start when the chart scrolls into view. |
40
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The draw-on, the ring morph and the rolling centre number are the flourish; the hover dim is the feedback `subtle` keeps. |
41
+ | animate | boolean | true | Draw the rings round from their start when the chart scrolls into view. `animate={false}` is `motion="subtle"`. |
41
42
  | activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
42
43
  | defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
43
44
  | onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
@@ -48,9 +49,10 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
48
49
 
49
50
  ## Motion
50
51
 
51
- - **arrival** · Ring · _custom (outside the recipe layer)_ — Once on screen each ring draws round from its start (stroke-dashoffset on a round-capped arc), staggered outside in. On the motion tokens; reduced motion shows the finished chart.
52
- - **response** · Active ring · _no motion_ — The hovered ring stays full strength and the others dim at once.
53
- - **transition** · Ring · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place.
52
+ - **arrival** · Ring · _custom (outside the recipe layer)_ — Once on screen each ring draws round from its start (stroke-dashoffset on a round-capped arc), staggered outside in. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.
53
+ - **response** · Active ring · _no motion_ — The hovered ring stays full strength and the others dim at once — mixed toward the surface rather than faded, so a dimmed ring stays solid over its track instead of turning muddy. Essential: `subtle` keeps it.
54
+ - **transition** · Centre value · _custom (outside the recipe layer)_ — A numeric centre value rolls to its new total digit by digit (NumberTicker) when the data changes, so the headline is seen moving with the rings instead of being found different afterwards. Expressive: under `subtle` it updates in place.
55
+ - **transition** · Ring · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place.
54
56
  - **activity** · RadialChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
55
57
 
56
58
  ## Accessibility
@@ -52,6 +52,15 @@ Use RadioCard for visual choice cards when users compare a small set of named op
52
52
 
53
53
  `input`, `card`, `foreground`, `primary`, `muted`, `ring`, `muted-foreground`, `tone-3`, `tone-2`, `tone-1`, `tone-info`, `tone-warning`
54
54
 
55
+ ## Motion
56
+
57
+ - **arrival** · Root · _no motion_ — Cards arrive with the form. A plan picker that animated each card in would delay the choice it exists to make.
58
+ - **response** · Root · `motion-interactive` — The edge steps toward the ring colour on hover, eased. On a card-sized target a hard snap across the whole border reads as the layout flickering.
59
+ - **response** · Root · `motion-press` — The settle on press confirms a click anywhere in the card — the radio is small, and without it a click in the card's body can feel like it missed the control.
60
+ - **transition** · Root · `motion-interactive` — Checking eases the tone ring and wash onto the new card as the old one releases, so the choice reads as moving between cards rather than two cards repainting.
61
+ - **transition** · Indicator · `motion-interactive` — The radio dot grows in on a spring, the same mark as Radio, so a card and a plain radio confirm a choice the same way.
62
+ - **activity** · Root · _no motion_ — A choice commits instantly. A slow write belongs to the surrounding form's pending state.
63
+
55
64
  ## Accessibility
56
65
 
57
66
  - Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.
@@ -40,6 +40,13 @@ Use Radio when a user must choose exactly one option from a visible set.
40
40
 
41
41
  `input`, `card`, `primary`, `ring`
42
42
 
43
+ ## Motion
44
+
45
+ - **arrival** · Root · _no motion_ — Radios arrive with the form they belong to. Animating each one in turns a question into a title sequence.
46
+ - **response** · Root · `motion-interactive` — The ring's edge and focus ring ease, so arrowing through a group — which moves the selection with focus — reads as one choice moving down the list.
47
+ - **transition** · Indicator · `motion-interactive` — The dot grows from the centre on a spring and shrinks out of the radio being left. A group's selection moves from one option to another, and the paired grow and shrink is what makes it read as moving rather than two dots blinking.
48
+ - **activity** · Root · _no motion_ — A radio commits instantly. A slow write belongs to the surrounding form's pending state, not the control.
49
+
43
50
  ## Accessibility
44
51
 
45
52
  - Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.
@@ -29,6 +29,14 @@ Use RelativeTime to show recent activity or timestamps in conversational relativ
29
29
 
30
30
  `popover`, `popover-foreground`, `border`, `ring`
31
31
 
32
+ ## Motion
33
+
34
+ - **arrival** · Root · _no motion_ — It is a word in a line of text and arrives with that line.
35
+ - **response** · Root · _no motion_ — The only hover change on the timestamp is a dotted underline, and text-decoration-line is discrete: CSS cannot tween it, so a transition here would animate nothing and imply the snap had been handled.
36
+ - **response** · Tooltip · `motion-pop` — The pointer's real answer is the absolute time, which arrives in a Tooltip on its own fast pop. That short entrance is what makes the full date read as belonging to the word under the cursor.
37
+ - **transition** · Root · _no motion_ — The label does not tick: it is computed when the component renders, so it never changes in front of the reader. A timestamp rolling from one minute to two would pull the eye off the content it dates.
38
+ - **activity** · Root · _no motion_ — A timestamp has nothing to wait for.
39
+
32
40
  ## Accessibility
33
41
 
34
42
  - Renders a semantic <time> element with a machine-readable dateTime (ISO) attribute.
@@ -41,6 +41,14 @@ Use Scroller to create a contained overflow region for content that naturally ex
41
41
 
42
42
  `ring`
43
43
 
44
+ ## Motion
45
+
46
+ - **arrival** · Root · _no motion_ — It is a region, not an event; its content arrives with whatever fills it.
47
+ - **response** · Viewport · `motion-interactive` — The region is focusable so the keyboard can scroll it, and its focus ring eases in like any control's. It is the one sign the region, not the page, now takes the arrow keys.
48
+ - **response** · Scrollbar · _custom (outside the recipe layer)_ — The vertical bar (`scrollbar-quiet`) stays hidden until the region is hovered, focused or scrolled, then fades its thumb in on the motion tokens. A bar that appeared in one frame would flash every time the pointer crossed the list.
49
+ - **transition** · Edge fade · _no motion_ — The edge fades switch on and off as you scroll, in the frame the content is already moving, so the change is carried by the scroll itself. They are a mask gradient, which CSS cannot interpolate without registered custom properties.
50
+ - **activity** · Root · _no motion_ — A scroll region does no work of its own. Content still loading inside it brings its own Skeleton or Spinner.
51
+
44
52
  ## Accessibility
45
53
 
46
54
  - The edge fades are a CSS mask-image on the scrolling element, not overlay elements — there is nothing in the DOM to intercept a pointer or be read by assistive tech, and the content under a fade stays fully interactive.
@@ -42,6 +42,14 @@ Use ScrubField for numeric properties in tool chrome — an inspector, a propert
42
42
 
43
43
  `foreground`, `muted-foreground`, `ring`, `panel-field`, `panel-field-hover`
44
44
 
45
+ ## Motion
46
+
47
+ - **arrival** · Root · _no motion_ — It arrives with the inspector row it sits in.
48
+ - **response** · Root · `motion-interactive` — The shell eases its focus ring like every field, so tabbing through a column of numbers shows where the keyboard is without flicker.
49
+ - **response** · Handle · `motion-interactive` — The handle's ink brightens while the field is hovered. It is the only sign that the icon or prefix can be dragged, which nobody would guess from a label.
50
+ - **transition** · Value · _no motion_ — The number follows the drag pixel for pixel. Easing it — or rolling it with a NumberTicker — would show a value the pointer has already left, in a control whose whole point is precision.
51
+ - **activity** · Root · _no motion_ — Scrubbing is synchronous; there is nothing to wait for.
52
+
45
53
  ## Accessibility
46
54
 
47
55
  - The input carries the accessible name via `label`; the glyph is aria-hidden because it is decorative.
@@ -36,6 +36,13 @@ Use SearchInput for text queries that filter or search a known content set.
36
36
 
37
37
  `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
38
38
 
39
+ ## Motion
40
+
41
+ - **arrival** · Clear button · `motion-appear` — The X mounts as the first character lands. Fading and settling in from a smaller scale keeps it from popping into the corner of the eye on every new search; removal is instant so an emptied field does not wait on it.
42
+ - **response** · Input · `motion-interactive` — The focus ring and invalid border ease in, so focus reads as the field answering rather than a repaint.
43
+ - **transition** · Root · _no motion_ — The field has no states of its own to move between — the clear button arriving is its only change, and arrival covers it.
44
+ - **activity** · Root · _no motion_ — Searching is the results' job: a list skeleton or a Spinner beside them, not a field that animates while you type.
45
+
39
46
  ## Accessibility
40
47
 
41
48
  - Renders a native <input type="search"> — keyboard and form semantics come for free.
@@ -30,6 +30,15 @@ Use ShowMore to reveal additional content after an intentionally shortened previ
30
30
 
31
31
  `background`, `foreground`, `ring`
32
32
 
33
+ ## Motion
34
+
35
+ - **arrival** · Root · _no motion_ — The preview arrives with the content it clips. Its fade and toggle appear only once a measurement proves the content overflows.
36
+ - **response** · Toggle · `motion-interactive` — The toggle is a link Button, so its hover and focus ease on Button's recipe.
37
+ - **transition** · Content · _custom (outside the recipe layer)_ — The region eases its max-height between the collapsed and the measured full height on the slow motion token. motion-disclosure keys on Base UI's starting and ending styles, which this self-contained toggle does not have. The text unrolling in place keeps the reader's line; a snap drops them at a different scroll position.
38
+ - **transition** · Fade · `motion-interactive` — The bottom fade dissolves as the region opens and returns as it closes, so the cut edge and the reveal read as one movement.
39
+ - **transition** · Chevron · `motion-interactive` — The chevron turns 180° in step with the region (motion-interactive lists `rotate`), so the toggle visibly points at what it will do next.
40
+ - **activity** · Root · _no motion_ — Expanding is local and instant; nothing loads.
41
+
33
42
  ## Accessibility
34
43
 
35
44
  - The toggle is a real <button> with aria-expanded reflecting the open state.
@@ -41,11 +41,19 @@ Use SideNav for vertical navigation inside a settings page, documentation area,
41
41
  | --- | --- | --- | --- |
42
42
  | size | "md" \| "lg" | — | Density shared by all items. "md" (default) = the lightweight in-content sub-nav; "lg" = chrome-weight (larger label + icon, roomier rows) for a rail that pairs with the app Sidebar. |
43
43
  | activeVariant | "content" \| "sidebar" | "content" | Active-state token vocabulary shared by all items. "content" uses bg-accent/text-accent-foreground for local rails inside the content area; "sidebar" uses bg-sidebar-accent/text-sidebar-accent-foreground for SideNavs rendered inside app chrome. |
44
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the current-page glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
44
45
 
45
46
  ## Tokens used
46
47
 
47
48
  `muted`, `muted-foreground`, `foreground`, `accent`, `accent-foreground`, `ring`, `sidebar-foreground`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
48
49
 
50
+ ## Motion
51
+
52
+ - **arrival** · Root · _no motion_ — In-page navigation arrives with the page it navigates.
53
+ - **response** · SideNavItem · `motion-interactive` — Rows ease their hover fill and focus ring, so moving down a long nav reads as one pointer travelling rather than rows flashing in turn.
54
+ - **transition** · SideNav · `motion-glide` — The current-item fill is one element that travels to the new row when the section changes inside a persistent layout, so the marker reads as moving rather than one row dimming while another lights. It lands in place on first paint, each row keeps its own fill as the server-render fallback, and the active row's heavier weight lands at once — font weight does not ease. Expressive: `subtle` moves it at once.
55
+ - **activity** · Root · _no motion_ — Navigation chrome has no working state; a loading section shows it in the content.
56
+
49
57
  ## Accessibility
50
58
 
51
59
  - SideNav renders a <nav> landmark; pair multiple navs with aria-label to distinguish them.
@@ -41,6 +41,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
41
41
  | icons | boolean | true | Render the leading icon on each SidebarItem. Set false for a text-only rail. Ignored while collapsed (icon-only by definition). |
42
42
  | variant | "app" \| "inline" | "app" | Where the rail lives. "app" is the page's chrome column — sticky, full viewport height, hidden below lg where AppHeader + BottomNav take over. "inline" is always visible and fills its parent's height, for a rail inside a bounded frame (a nested shell, an embedded preview). Use it instead of overriding the layout with !important classes. |
43
43
  | surface | "rail" \| "page" | "rail" | Which surface the rail is painted on. "rail" is its own --sidebar-* surface with a border — right for a full-height rail that frames the app, which a theme may tint or paint dark. "page" uses the page's surface and ink with no border — use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat. |
44
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the collapse ease and the current-page glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
44
45
 
45
46
  ## Variants
46
47
 
@@ -50,6 +51,14 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
50
51
 
51
52
  `sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
52
53
 
54
+ ## Motion
55
+
56
+ - **arrival** · Root · _no motion_ — The rail is the app's frame, present from first paint. It is not summoned, so it has no entrance.
57
+ - **response** · SidebarItem · `motion-interactive` — Rows ease their hover fill (SideNav's row recipe on the rail rung), so moving down the nav reads as the pointer travelling rather than rows flashing.
58
+ - **transition** · SidebarNav · `motion-glide` — The current-page fill is one element that travels to the new item when the page changes inside a persistent layout, so the marker reads as moving rather than one row dimming while another lights. It lands in place on first paint, and each item keeps its own fill as the server-render fallback. Expressive: `subtle` moves it at once.
59
+ - **transition** · Root · `motion-size` — Collapsing to the icon rail eases the width instead of jumping 192px in one frame, so the content beside it visibly takes the room the rail gave up. Nothing inside jumps — a collapsed row's centred icon and an expanded row's inset icon sit on the same column — and labels leave at once and fade back in on reopen, clipped to the rail meanwhile. It reflows the page for the length of the ease, which is acceptable for a deliberate user toggle and never happens on load. Expressive: `subtle` snaps.
60
+ - **activity** · Root · _no motion_ — Navigation chrome has no working state; a loading route shows it in the page.
61
+
53
62
  ## Accessibility
54
63
 
55
64
  - SidebarNav renders a <nav> landmark.
@@ -60,6 +60,13 @@ Use Slider for bounded numeric choices where approximate adjustment is faster th
60
60
 
61
61
  `secondary`, `primary`, `background`, `ring`
62
62
 
63
+ ## Motion
64
+
65
+ - **arrival** · Root · _no motion_ — It arrives with the form or panel it sits in.
66
+ - **response** · Thumb · `motion-interactive` — The thumb's dot grows from 12px to full size on hover and keyboard focus, and a little past it while dragging, on a spring. Only the paint resizes, never the hit target, so the growth says the thumb is live without moving where you have to aim.
67
+ - **transition** · Thumb · _no motion_ — The thumb and the filled range follow the pointer one-to-one, and keyboard steps land at once. An eased position would trail the drag and show a value the user has already moved past.
68
+ - **activity** · Root · _no motion_ — A slider commits as it moves; there is nothing to wait for.
69
+
63
70
  ## Accessibility
64
71
 
65
72
  - Base UI provides role=slider, aria-valuemin/max/now, and arrow-key / Page Up-Down / Home-End control.
@@ -26,7 +26,8 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
26
26
  | variant | "line" \| "area" \| "bar" | "line" | A line, a line over a soft fill, or columns. |
27
27
  | tone | "1" \| "2" \| "3" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | "primary" | Colour by role. |
28
28
  | className | string | "h-8 w-24" | Sizes it; the drawing stretches to fit. |
29
- | animate | boolean | true | Draw on (or rise) when it scrolls into view. |
29
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Drawing on (or rising) is the flourish, so `subtle` and `off` show the finished line. |
30
+ | animate | boolean | true | Draw on (or rise) when it scrolls into view. `animate={false}` is `motion="subtle"`. |
30
31
  | locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
31
32
 
32
33
  ## Variants
@@ -39,7 +40,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
39
40
 
40
41
  ## Motion
41
42
 
42
- - **arrival** · Line / bars · _custom (outside the recipe layer)_ — Once on screen the line draws on (or the bars rise, in sequence). On the motion tokens; reduced motion shows the finished drawing.
43
+ - **arrival** · Line / bars · _custom (outside the recipe layer)_ — Once on screen the line draws on (or the bars rise, in sequence). On the motion tokens; reduced motion shows the finished drawing. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished line.
43
44
  - **response** · Sparkline · _no motion_ — Not interactive: it has no tooltip or focus.
44
45
  - **transition** · Sparkline · _no motion_ — New data redraws in place; a word-sized chart animating on every update would distract from the number beside it.
45
46
  - **activity** · Sparkline · _no motion_ — No indeterminate state.
@@ -50,6 +50,13 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
50
50
 
51
51
  `border`, `muted`, `muted-foreground`
52
52
 
53
+ ## Motion
54
+
55
+ - **arrival** · Root · _no motion_ — A table renders with the page it sits in. Staggering rows in delays reading data someone opened to scan.
56
+ - **response** · TableRow · `motion-interactive` — Body rows ease to a muted wash under the pointer. Across a wide row, the wash is what keeps the eye on the right line, and eased it follows the cursor down the table rather than strobing.
57
+ - **transition** · TableCell · `motion-interactive` — A `data-selected` row's fill eases in on every cell together, so the selected row reads as one band being set rather than a stamp.
58
+ - **activity** · Root · _no motion_ — Table is markup. Loading rows are Skeletons, which carry the shimmer.
59
+
53
60
  ## Accessibility
54
61
 
55
62
  - Renders semantic native table elements (thead/tbody/tfoot/th/td/caption) preserving built-in table semantics.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- Tabs over Base UI Tabs (roving focus, arrow-key nav, ARIA tablist wiring) in two styles: underline (page sections) or pill (compact switchers). Tabs (Root) + TabsList + TabsTrigger + TabsPanel.
5
+ Tabs over Base UI Tabs (roving focus, arrow-key nav, ARIA tablist wiring) in two styles: underline (page sections) or pill (compact switchers), with one indicator that glides to the active tab. Tabs (Root) + TabsList + TabsTrigger + TabsPanel.
6
6
 
7
7
  **Keywords:** tabs, tablist, panels, segmented content
8
8
 
@@ -31,7 +31,7 @@ Use Tabs to switch between related panels within the same page context.
31
31
 
32
32
  | Name | Type | Description |
33
33
  | --- | --- | --- |
34
- | TabsList | Base UI Tabs.List props (activateOnFocus, loopFocus) + variant?: "underline" \| "pill" + className | The tablist row. variant="underline" (default) draws a divider rule beneath the triggers; variant="pill" drops the rule and fills the active trigger, which takes the theme's button shape (--button-radius). Triggers inherit the variant. |
34
+ | TabsList | Base UI Tabs.List props (activateOnFocus, loopFocus) + variant?: "underline" \| "pill" + className | The tablist row. variant="underline" (default) draws a divider rule beneath the triggers; variant="pill" drops the rule and fills the active trigger, which takes the theme's button shape (--button-radius). Triggers inherit the variant. It renders the gliding indicator (Base UI Tabs.Indicator) itself, so do not add one. |
35
35
  | TabsTrigger | Base UI Tabs.Tab props (value, disabled) + className | A single tab button. value pairs it with the panel of the same value. |
36
36
  | TabsPanel | Base UI Tabs.Panel props (value, keepMounted) + className | The content shown when the matching trigger is active. |
37
37
  | TabsContent | Alias of TabsPanel | Re-export of TabsPanel for consumers that prefer this name. |
@@ -41,17 +41,27 @@ Use Tabs to switch between related panels within the same page context.
41
41
  | Name | Type | Default | Description |
42
42
  | --- | --- | --- | --- |
43
43
  | Tabs (Root) | Base UI Tabs.Root props (value, defaultValue, onValueChange, orientation) | defaultValue: 0 | Controls the active tab. Uncontrolled via defaultValue; controlled via value + onValueChange. |
44
+ | motion | "full" \| "subtle" \| "off" | "full" | On Tabs: how much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
44
45
 
45
46
  ## Tokens used
46
47
 
47
48
  `border`, `muted`, `muted-foreground`, `foreground`, `ring`
48
49
 
50
+ ## Motion
51
+
52
+ - **arrival** · Root · _no motion_ — Tabs arrive with the section they organise. An entrance of their own would announce navigation chrome louder than the content it navigates.
53
+ - **response** · TabsTrigger · `motion-interactive` — The label steps from muted to foreground under the pointer; the ease is what makes that read as an answer rather than a repaint.
54
+ - **transition** · Indicator · `motion-glide` — One underline (or pill) travels to the new tab and stretches to its width. A mark that teleports makes the reader re-find the selection on every click; one that moves is tracked without looking. Expressive: motion="subtle" lands it in place. Each tab's own mark is the fallback until the indicator has measured, so the server render is already right.
55
+ - **transition** · TabsPanel · `animate-fade-in` — The incoming panel fades in, opacity only, so a switch between two similar panels still registers. Skipped on first render (activation direction "none") so a page of tabs does not fade in on load, and the outgoing panel leaves layout at once so a quick second click never stacks both.
56
+ - **activity** · Root · _no motion_ — Switching tabs is synchronous. A panel that loads data shows its own Skeleton or Spinner inside the panel, where the wait is.
57
+
49
58
  ## Accessibility
50
59
 
51
60
  - Base UI provides the tablist/tab/tabpanel roles, aria-selected, and roving tabindex.
52
61
  - Arrow keys move between tabs; Home/End jump to first/last; panels are linked via aria-controls.
53
62
  - Active tab styling reads from the data-active attribute; disabled tabs from data-disabled. Disabled tabs are skipped by arrow-key navigation.
54
63
  - Keyboard focus shows a ring on the focused trigger in both variants; the panel remains focusable.
64
+ - The gliding indicator is role="presentation" decoration; aria-selected on the tab carries the state.
55
65
 
56
66
  ## Related
57
67
 
@@ -82,3 +92,11 @@ import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
82
92
 
83
93
  <Tabs defaultValue="overview"><TabsList><TabsTrigger value="overview">Overview</TabsTrigger><TabsTrigger value="billing" disabled>Billing</TabsTrigger></TabsList><TabsPanel value="overview">Overview content</TabsPanel></Tabs>
84
94
  ```
95
+
96
+ ### Subtle motion
97
+
98
+ ```tsx
99
+ import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
100
+
101
+ <Tabs defaultValue="table" motion="subtle"><TabsList><TabsTrigger value="table">Table</TabsTrigger><TabsTrigger value="chart">Chart</TabsTrigger></TabsList><TabsPanel value="table">Rows</TabsPanel><TabsPanel value="chart">Trend</TabsPanel></Tabs>
102
+ ```
@@ -49,6 +49,14 @@ Use ThemeSelector wherever a person picks a theme. The default `cards` layout is
49
49
 
50
50
  `input`, `card`, `border`, `background`, `primary`, `primary-foreground`, `accent`, `muted`, `muted-foreground`, `ring`
51
51
 
52
+ ## Motion
53
+
54
+ - **arrival** · Root · _no motion_ — It sits in a settings panel or a popover, which owns the entrance.
55
+ - **response** · Option (list) · `motion-interactive` — List rows ease their hover fill like Menu and Select rows, so a theme list beside a menu reads as the same kind of row.
56
+ - **response** · Name (grid) · `motion-interactive` — In the grid, the theme's name brightens on hover, eased. The preview tile's outline steps rather than easing — outline is not in the recipe's transition list — so the name carries the motion.
57
+ - **transition** · Check (list) · `motion-interactive` — The check fades into the chosen row and out of the old one, so the choice reads as moving, at the same moment the whole interface repaints in the new theme.
58
+ - **activity** · Root · _no motion_ — Applying a theme is synchronous: the whole interface repaints at once, and that repaint is the feedback.
59
+
52
60
  ## Accessibility
53
61
 
54
62
  - The container is role="radiogroup" with an aria-label; each card is a role="radio" button with aria-checked reflecting the selected theme.
@@ -25,12 +25,21 @@ Use ThemeSwitcher for light, dark, and system color mode preference.
25
25
  | value | "light" \| "dark" \| "system" | — | The currently selected color preference. |
26
26
  | onValueChange | (value: ThemeMode) => void | — | Called with the next color preference when an option is chosen. |
27
27
  | label | string | "Theme" | Accessible label for the radiogroup. |
28
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
28
29
  | className | string | — | Merged onto the outer radiogroup container. |
29
30
 
30
31
  ## Tokens used
31
32
 
32
33
  `border`, `card`, `muted-foreground`, `foreground`, `accent`, `accent-foreground`, `ring`
33
34
 
35
+ ## Motion
36
+
37
+ - **arrival** · Root · _no motion_ — It arrives with the header or settings row it lives in; a theme control that announces itself competes with the page it themes.
38
+ - **response** · Option · `motion-interactive` — The icon steps from muted to foreground under the pointer, which is what tells an icon-only segment it is a control.
39
+ - **response** · Option · `motion-press` — A settle under the press confirms the tap on a 28px target before the theme change repaints the page around it.
40
+ - **transition** · Active pill · `motion-glide` — One pill slides to the chosen mode, stepping by option + gap on the spacing scale, so light → dark reads as the same control moving, not three icons trading a fill. Expressive: motion="subtle" lands it in place.
41
+ - **activity** · Root · _no motion_ — Changing the theme is synchronous; the repaint is the feedback.
42
+
34
43
  ## Accessibility
35
44
 
36
45
  - The container is role="radiogroup" with an aria-label; each option is role="radio" with aria-checked reflecting the selected value.
@@ -69,3 +78,11 @@ import { ThemeSwitcher } from "@sableui/react"
69
78
 
70
79
  <ThemeSwitcher value="light" onValueChange={setTheme} />
71
80
  ```
81
+
82
+ ### Subtle motion
83
+
84
+ ```tsx
85
+ import { ThemeSwitcher } from "@sableui/react"
86
+
87
+ <ThemeSwitcher value={theme} onValueChange={setTheme} motion="subtle" />
88
+ ```
@@ -30,15 +30,16 @@ Use Toggle for button-like controls that can be pressed on or off within a toolb
30
30
 
31
31
  | Name | Type | Description |
32
32
  | --- | --- | --- |
33
- | ToggleGroup | Base UI ToggleGroup props (value, defaultValue, onValueChange, multiple, disabled, orientation, loopFocus) + appearance?: "default" \| "segmented" + className | Groups child Toggles and shares pressed state across them — each Toggle inside carries a value. Defaults to multiple: false (single-select) — the fit for view-mode switches and segmented controls; pass multiple to allow several pressed at once, e.g. a row of formatting toggles. |
33
+ | ToggleGroup | Base UI ToggleGroup props (value, defaultValue, onValueChange, multiple, disabled, orientation, loopFocus) + appearance?: "default" \| "segmented" + motion?: "full" \| "subtle" \| "off" + className | Groups child Toggles and shares pressed state across them — each Toggle inside carries a value. Defaults to multiple: false (single-select) — the fit for view-mode switches and segmented controls; pass multiple to allow several pressed at once, e.g. a row of formatting toggles. |
34
34
  | ToggleGroupItem | Alias of Toggle | Re-export of Toggle for group composition — a Toggle carrying a value is the group item. |
35
35
 
36
36
  ## Props
37
37
 
38
38
  | Name | Type | Default | Description |
39
39
  | --- | --- | --- | --- |
40
- | appearance | "default" \| "segmented" | "default" | On ToggleGroup: `default` is a loose row of toggles; `segmented` draws the capsule segmented control (one track, the pressed option filled) shared with ThemeSwitcher. Toggles inside inherit it. |
40
+ | appearance | "default" \| "segmented" | "default" | On ToggleGroup: `default` is a loose row of toggles; `segmented` draws the capsule segmented control (one track, the pressed option filled) shared with ThemeSwitcher. Single-select segmented draws ONE fill that glides to the pressed option and resizes to its label; multi-select keeps a fill per pressed option. Toggles inside inherit it. |
41
41
  | multiple | boolean | false | On ToggleGroup: allow several toggles pressed at once. The default is single-select, which behaves like a radio group. |
42
+ | motion | "full" \| "subtle" \| "off" | "full" | On ToggleGroup: how much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
42
43
  | Toggle | Base UI Toggle props (pressed, defaultPressed, onPressedChange, disabled, value) + className | defaultPressed: false | A two-state button. Uncontrolled via defaultPressed; controlled via pressed + onPressedChange. Pass value to identify it inside a ToggleGroup. |
43
44
 
44
45
  ## Variants
@@ -49,6 +50,14 @@ Use Toggle for button-like controls that can be pressed on or off within a toolb
49
50
 
50
51
  `muted-foreground`, `foreground`, `accent`, `accent-foreground`, `card`, `border`, `ring`
51
52
 
53
+ ## Motion
54
+
55
+ - **arrival** · Toggle · _no motion_ — Toggles arrive with the toolbar or header they sit in. Animating each one in would make a row of controls ripple for no reason.
56
+ - **response** · Toggle · `motion-interactive` — Hover and the pressed fill ease rather than flip, so pressing reads as the same button changing state, not a different button appearing.
57
+ - **response** · Toggle · `motion-press` — The small settle under the finger confirms the press landed before the state change is read — the difference matters most on touch, where there is no hover.
58
+ - **transition** · Segmented fill · `motion-glide` — In a single-select segmented group one fill travels to the chosen option and stretches to its label, so a view switch reads as one control now pointing elsewhere. Measured, because labels differ in width. Expressive: motion="subtle" lands it in place.
59
+ - **activity** · Toggle · _no motion_ — A toggle commits instantly. Work that follows from the new state shows where its result appears, not on the control.
60
+
52
61
  ## Accessibility
53
62
 
54
63
  - Base UI sets aria-pressed on each Toggle and manages the pressed/disabled states.
@@ -86,6 +95,14 @@ import { Icon, Toggle, ToggleGroup } from "@sableui/react"
86
95
  <ToggleGroup appearance="segmented" defaultValue={["list"]} aria-label="View"><Toggle value="list" aria-label="List view"><Icon name="list" /></Toggle><Toggle value="grid" aria-label="Grid view"><Icon name="grid" /></Toggle></ToggleGroup>
87
96
  ```
88
97
 
98
+ ### Segmented with labels, subtle motion
99
+
100
+ ```tsx
101
+ import { Toggle, ToggleGroup } from "@sableui/react"
102
+
103
+ <ToggleGroup appearance="segmented" motion="subtle" defaultValue={["week"]} aria-label="Range"><Toggle value="day">Day</Toggle><Toggle value="week">Week</Toggle><Toggle value="month">Month</Toggle></ToggleGroup>
104
+ ```
105
+
89
106
  ### Multi-select group
90
107
 
91
108
  ```tsx