@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
@@ -30,6 +30,7 @@ Use Calendar to browse or choose dates in a month grid, especially when relative
30
30
  | disabled | Matcher \| Matcher[] | — | Days matching this are dimmed and non-selectable (e.g. { before: new Date() }). |
31
31
  | showOutsideDays | boolean | true | Render leading/trailing days from adjacent months, dimmed. |
32
32
  | bare | boolean | false | Drop the card frame (border, fill, padding) so the grid sits flush inside a surface that already has one, such as a Popover. DatePicker renders its Calendar this way. |
33
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the month slide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
33
34
  | className | string | — | Merged onto the bordered card container that wraps the grid. |
34
35
  | classNames | Partial<ClassNames> | — | Per-part class overrides merged over the themed defaults. Themed slots: months, month, nav, button_previous, button_next, month_caption, caption_label, month_grid, weekdays, weekday, week, day, day_button, range_start, range_middle, range_end, selected, today, outside, disabled, hidden. Selection/state slots (selected/today/range_*/outside/disabled) style the day CELL; the inner day button is targeted via the [&>button] descendant. |
35
36
  | components | Partial<CustomComponents> | — | Render overrides for RDP parts. Chevron is supplied by default and merged with yours. |
@@ -42,6 +43,14 @@ Use Calendar to browse or choose dates in a month grid, especially when relative
42
43
 
43
44
  `card`, `card-foreground`, `border`, `primary`, `primary-foreground`, `accent`, `accent-foreground`, `muted-foreground`, `ring`
44
45
 
46
+ ## Motion
47
+
48
+ - **arrival** · Root · _no motion_ — Inline, it arrives with its form; in a DatePicker the Popover carries the entrance. A grid that animated in by itself would put a second motion on the same opening.
49
+ - **response** · Day · `motion-interactive` — Days ease their hover wash, and the month arrows are ghost Buttons on Button's own recipe. Across a seven-column grid the pointer crosses many cells at once, and an eased wash reads as a cursor travelling rather than a grid flashing.
50
+ - **transition** · Day · `motion-interactive` — Picking a day eases the primary fill onto it and off the previous one, so a click reads as the selection moving rather than two cells flipping. A range's band is painted on the cells, which carry the same recipe, so the band eases along with its endpoints as the range grows.
51
+ - **transition** · Month · _custom (outside the recipe layer)_ — Paging months slides the outgoing grid toward the edge it is leaving by and brings the new one in from the other, over a short travel with a fade (react-day-picker's `animate` mode on Sable keyframes in styles.css), so forward and back read as direction rather than a grid swapping in one frame. RDP adds a single class per part, which a recipe utility cannot be; mirrored in RTL. Expressive: Calendar turns `animate` on only at full motion, and an explicit `animate` prop still wins.
52
+ - **activity** · Root · _no motion_ — Days are resident data; nothing in the grid loads. Availability fetched per month belongs to the surface around the calendar.
53
+
45
54
  ## Accessibility
46
55
 
47
56
  - react-day-picker manages the roving-tabindex grid, arrow-key navigation, and ARIA roles/labels for the calendar.
@@ -36,11 +36,20 @@ Use Carousel for a curated, finite sequence of media or cards that benefits from
36
36
  | label | string | — | Accessible label for the carousel region. |
37
37
  | slideClassName | string | — | Class for each slide wrapper (e.g. an aspect ratio). |
38
38
  | arrowClassName | string | — | Class for both prev/next arrow buttons. Merged through cn(), so a radius or shadow utility replaces the default rounded-full shadow-md rather than stacking with it. |
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 glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Under subtle the slides still travel and the active dot snaps; off also cuts between slides and pauses autoplay. |
39
40
 
40
41
  ## Tokens used
41
42
 
42
43
  `border`, `ring`, `popover`, `popover-foreground`, `foreground`, `muted-foreground`
43
44
 
45
+ ## Motion
46
+
47
+ - **arrival** · Root · _no motion_ — A carousel arrives with its section. Its content already moves on every advance; an entrance on top would make the first view feel like an advance nobody asked for.
48
+ - **response** · Dot · `motion-interactive` — The dot's 24px hit area eases its focus ring and the dot brightens under the pointer, which is how a 6px mark tells you it is a button.
49
+ - **transition** · Track · _custom (outside the recipe layer)_ — The slide track's translate is set inline from the measured slide pitch, on --motion-duration-slow and the emphasized easing, and switched off during a drag so the track follows the finger. A recipe cannot carry a measured, drag-interrupted offset. Essential: it is the answer to "next", so it plays under subtle.
50
+ - **transition** · Dot · `motion-glide` — The active dot stretches to a bar while the one it left shrinks, on one timeline, so the row keeps its length and the mark reads as travelling with the slide. It used to change width unanimated and jumped while the slides beside it glided. Expressive: motion="subtle" snaps it.
51
+ - **activity** · Root · _no motion_ — Autoplay's progress is the slides themselves advancing and the play/pause control; a separate timer animation would put a second clock beside the first.
52
+
44
53
  ## Accessibility
45
54
 
46
55
  - Region carries aria-roledescription='carousel'; each slide is a group with aria-roledescription='slide' and an 'N of M' label; clones are aria-hidden.
@@ -85,3 +94,11 @@ import { Carousel } from "@sableui/react"
85
94
 
86
95
  <Carousel loop autoplay={4000} counter>{slides}</Carousel>
87
96
  ```
97
+
98
+ ### Subtle motion
99
+
100
+ ```tsx
101
+ import { Carousel } from "@sableui/react"
102
+
103
+ <Carousel loop motion="subtle" label="Gallery">{slides}</Carousel>
104
+ ```
@@ -58,6 +58,7 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
58
58
  | stopLabel | string | "Stop generating" | Accessible label for the stop button. |
59
59
  | footer | ReactNode \| ((state: { sendOn: "enter" \| "mod-enter" }) => ReactNode) | — | Helper text or counter below the controls. For a keyboard hint pass a function: it receives the send key in effect (touch turns `enter` into `mod-enter`), and returning null removes the row, e.g. `footer={({ sendOn }) => sendOn === "enter" ? "Enter to send" : null}`. |
60
60
  | fieldRef | Ref<HTMLTextAreaElement> | — | Ref to the underlying textarea — for focusing after a send, or on mount, and the extension point for anything that listens to the field. Pass `useCaretTrigger`'s `fieldRef` here for mentions and slash commands (CaretMenu). An Enter another listener already handled (`preventDefault()`) does not send. |
61
+ | 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. Under `subtle` the field snaps to its new height and the send/stop glyphs swap without the morph. |
61
62
  | ...form props | Omit<ComponentPropsWithRef<"form">, "onSubmit" \| "defaultValue"> | — | Spread onto the form element. Use `onSend` rather than `onSubmit`. |
62
63
 
63
64
  ## Variants
@@ -68,6 +69,14 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
68
69
 
69
70
  `card`, `input`, `ring`, `destructive`, `foreground`, `muted-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`
70
71
 
72
+ ## Motion
73
+
74
+ - **arrival** · Root · _no motion_ — The composer is the fixed foot of a thread, there before anything else is. An entrance would only delay the first keystroke.
75
+ - **response** · Root · `motion-interactive` — The surface's focus ring and invalid border ease in, so focusing the field reads as the whole box waking up rather than a frame flicker.
76
+ - **transition** · Field · _custom (outside the recipe layer)_ — As a line breaks or a paste lands, the field eases to its new height (up to its cap) instead of jumping, so the transcript above is visibly pushed rather than teleported. `field-sizing` alone cannot ease — its height stays `auto` — so a small hook measures and pins a pixel height. Expressive and short (`fast`): the text itself lands at once, and `subtle` snaps the box.
77
+ - **transition** · Send button · `motion-icon-swap` — Send turning into stop is the moment a reply starts, and stop turning back is the moment it is over. One button changes role in place — the fill eases, the arrow collapses into the square — so it reads as the same control changing what it does, not a different button appearing under the cursor.
78
+ - **activity** · Send button · `motion-spin` — Busy with no stop handler, Button's own loading Spinner replaces the arrow, so a send in flight is not mistaken for a disabled one.
79
+
71
80
  ## Accessibility
72
81
 
73
82
  - A real `<form>`, so the send button is a submit button and the whole thing behaves as one control to assistive tech and to password managers alike.
@@ -43,6 +43,7 @@ Use CheckboxCard when an option needs more visual weight, supporting text, or ic
43
43
  | tone | "neutral" \| "3" \| "2" \| "1" \| "info" \| "warning" | "neutral" | Semantic colour the checked state carries across the border, checkbox box, and icon. |
44
44
  | value | string | — | Names the card inside a group-owned CheckboxGroup: the card is checked when the group's value array includes it. Also the value submitted with a form. Inside a CheckboxGroup, `name` stands in for `value` when `value` is omitted. |
45
45
  | checked / onCheckedChange / defaultChecked / disabled | Base UI Checkbox.Root props | — | Standard checkbox state. Without a group-owned value, each card is independent — drive checked/onCheckedChange per item for multi-select. |
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. Under `subtle` the tick fades in instead of drawing itself. |
46
47
 
47
48
  ## Variants
48
49
 
@@ -52,6 +53,13 @@ Use CheckboxCard when an option needs more visual weight, supporting text, or ic
52
53
 
53
54
  `input`, `card`, `ring`, `primary`, `primary-foreground`, `accent`, `foreground`, `muted-foreground`, `tone-3`, `tone-3-foreground`, `tone-2`, `tone-2-foreground`, `tone-1`, `tone-1-foreground`, `tone-info`, `tone-info-foreground`, `tone-warning`, `tone-warning-foreground`
54
55
 
56
+ ## Motion
57
+
58
+ - **arrival** · Root · _no motion_ — It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence.
59
+ - **response** · Box · `motion-interactive` — The box fills with the primary colour as it is checked; easing the fill keeps the mark and the box changing as one event rather than two frames apart.
60
+ - **transition** · Mark · `motion-tick` — The check draws itself in from its leading stroke, so checking reads as the box being ticked rather than a glyph appearing. The dash wipes the same way. Unchecking only fades, so clearing a list is quick; under `subtle` checking fades too.
61
+ - **activity** · Root · _no motion_ — A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control.
62
+
55
63
  ## Accessibility
56
64
 
57
65
  - Base UI provides the checkbox role, aria-checked, hidden native input, keyboard toggle, and data-[checked]/data-disabled state.
@@ -31,11 +31,19 @@ Use Checkbox for independent boolean choices or for selecting multiple options i
31
31
  | name | string | — | Field name for the hidden input on form submit. |
32
32
  | value | string | — | Value submitted with the form when checked. Inside a group-owned CheckboxGroup, it is the name the group's value array uses for this checkbox. |
33
33
  | render | ReactElement \| (props, state) => ReactElement | — | Compose the root with another element through the Base UI render prop. |
34
+ | 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. Under `subtle` the tick fades in instead of drawing itself. |
34
35
 
35
36
  ## Tokens used
36
37
 
37
38
  `card`, `input`, `primary`, `primary-foreground`, `ring`
38
39
 
40
+ ## Motion
41
+
42
+ - **arrival** · Root · _no motion_ — It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence.
43
+ - **response** · Box · `motion-interactive` — The box fills with the primary colour as it is checked; easing the fill keeps the mark and the box changing as one event rather than two frames apart.
44
+ - **transition** · Mark · `motion-tick` — The check draws itself in from its leading stroke, so checking reads as the box being ticked rather than a glyph appearing. The dash wipes the same way. Unchecking only fades, so clearing a list is quick; under `subtle` checking fades too.
45
+ - **activity** · Root · _no motion_ — A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control.
46
+
39
47
  ## Accessibility
40
48
 
41
49
  - Base UI renders a hidden native <input type=checkbox> for keyboard, focus, and form semantics.
@@ -45,10 +45,18 @@ Use CodeBlock for multiline code samples that users may read, inspect, or copy.
45
45
 
46
46
  `border`, `muted`, `muted-foreground`, `foreground`
47
47
 
48
+ ## Motion
49
+
50
+ - **arrival** · Root · _no motion_ — Code is reading content and arrives with the page or message it belongs to. An entrance of its own would delay the text someone came to copy.
51
+ - **response** · Copy button · `motion-interactive` — The copy button is a quiet ghost icon in the corner of a block. Its hover fill is what tells the pointer it is a control and not decoration.
52
+ - **transition** · Copy glyph · `motion-icon-swap` — The copy mark turning into a check is the only proof the clipboard took the text. Snapped between frames it was easy to miss at the edge of a block; the swap reads as one mark being replaced.
53
+ - **activity** · Root · _no motion_ — Copying is instant. There is no in-between worth showing, and a spinner for a clipboard write would read as a slow one.
54
+
48
55
  ## Accessibility
49
56
 
50
57
  - Code renders inside a <pre><code> for correct semantics and screen-reader handling.
51
58
  - Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety.
59
+ - A successful copy is announced as "Copied" through a polite role=status region that is always mounted; the button keeps its label, which names what it does. Both glyphs are aria-hidden.
52
60
 
53
61
  ## Related
54
62
 
@@ -45,6 +45,14 @@ Use Collapsible for a single piece of optional content that can expand in place.
45
45
 
46
46
  `foreground`, `muted-foreground`
47
47
 
48
+ ## Motion
49
+
50
+ - **arrival** · Root · _no motion_ — A collapsible is part of the page it sits in, present from first paint. Its trigger is what the reader notices, not an entrance.
51
+ - **response** · CollapsibleTrigger · _no motion_ — The trigger is whatever the consumer renders (usually a Button), and that control brings its own response. Adding one here would double it.
52
+ - **transition** · CollapsiblePanel · `motion-disclosure` — The height change ties the content that appears to the trigger that was pressed; snapped open, everything below jumps with nothing linking cause to effect.
53
+ - **transition** · CollapsibleIndicator · `motion-interactive` — The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180 writes.
54
+ - **activity** · Root · _no motion_ — Nothing here is indeterminate; content that loads on open should put its own loader inside the panel.
55
+
48
56
  ## Accessibility
49
57
 
50
58
  - Base UI wires aria-expanded on the trigger and aria-controls/id between trigger and panel.
@@ -42,6 +42,13 @@ Use ColorField wherever a colour is a property: a canvas background, an effect p
42
42
 
43
43
  `panel`, `panel-raised`, `hairline`, `hairline-strong`, `ring`, `popover`
44
44
 
45
+ ## Motion
46
+
47
+ - **arrival** · Popover · `motion-pop` — The picker grows out of the swatch it edits (PopoverContent's recipe, its origin on the anchor). In a property panel with a column of swatches, that origin is what tells you which colour you are now editing.
48
+ - **response** · Swatch · `motion-interactive` — Preset swatches lift slightly under the pointer. They are 20px chips of colour with no label, and the eased lift is what tells you which one a click will take.
49
+ - **transition** · Handles · _no motion_ — The saturation, hue and alpha handles follow the pointer exactly. A handle that trailed the drag would misreport the colour under the cursor, and a swatch or a typed hex moving them is the value changing, which the field should show at once.
50
+ - **activity** · Root · _no motion_ — Picking is synchronous. The EyeDropper is the browser's own modal, with its own feedback.
51
+
45
52
  ## Accessibility
46
53
 
47
54
  - The trigger's accessible name includes the current value, so a screen reader announces the colour rather than just "button" — the swatch itself conveys nothing without sight.
@@ -64,6 +64,16 @@ Use Combobox when users choose from a list that benefits from typing, filtering,
64
64
 
65
65
  `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`, `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `primary`, `secondary`, `secondary-foreground`
66
66
 
67
+ ## Motion
68
+
69
+ - **arrival** · ComboboxContent · `motion-pop` — The list grows from the field it filters, its origin on Base UI's anchor. With several comboboxes in a form, that is what attaches the open list to the one being typed in.
70
+ - **response** · ComboboxItem · `motion-interactive` — A row's ink eases as the pointer or the arrow keys reach it, so moving through a long filtered list reads as continuous movement rather than rows blinking.
71
+ - **response** · Highlight · _custom (outside the recipe layer)_ — One highlight travels to the active row (lib/highlight-glide: a measured translate on the motion tokens) instead of each row painting its own fill. When the list re-filters under the caret, the highlight sliding to the new match is what shows the keyboard target moved.
72
+ - **response** · Toggle · `motion-interactive` — The chevron toggle at the field's edge eases its ink on hover; the change is what tells you it is a separate target from the text box.
73
+ - **transition** · Toggle · `motion-interactive` — The chevron turns 180° as the list opens and closes (motion-interactive lists `rotate`), in step with the popup, so it reads as one control changing direction.
74
+ - **transition** · ComboboxContent · `motion-pop` — Closing shrinks back toward the field, returning the eye to the input that now holds the value.
75
+ - **activity** · Root · _no motion_ — Combobox ships no loading part. A list fetched as you type shows a Spinner (motion-spin) in ComboboxEmpty or the content — where the results will land — and only the caller knows a fetch is in flight.
76
+
67
77
  ## Accessibility
68
78
 
69
79
  - Base UI provides role=combobox/listbox/option, roving tabindex, arrow-key navigation, and type-to-filter.
@@ -70,6 +70,15 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
70
70
 
71
71
  `card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
72
72
 
73
+ ## Motion
74
+
75
+ - **arrival** · Dialog · `motion-pop` — The palette opens on DialogContent's recipe over a fading backdrop. It is summoned by a keystroke from nowhere on screen, and the short scale-in is what says a layer arrived rather than the page changing. The inline CommandMenuPanel has no entrance: it is part of the page.
76
+ - **arrival** · Clear button · `motion-appear` — The clear button mounts on the first keystroke, beside the caret the eye is already on. Fading in, it reads as the field offering a way back instead of a control blinking into the header. Removal is instant, so an emptied query does not wait on its X.
77
+ - **response** · CommandItem · `motion-interactive` — The active row follows both the arrow keys and the pointer, and its fill eases between rows, so scanning results while typing reads as one highlight moving rather than rows flashing in turn.
78
+ - **response** · CommandMenuTrigger · `motion-interactive` — The search-shaped trigger eases its ink on hover like the field shell it imitates, so it reads as the field it opens.
79
+ - **transition** · Dialog · `motion-pop` — Closing on Enter or Esc shrinks the palette away fast and returns focus where it came from; the exit is the confirmation that the command ran or was dismissed.
80
+ - **activity** · List · _no motion_ — Results are filtered synchronously from the items passed in. Async results are the caller's to mark with a Spinner (motion-spin) in the list, where the person is already looking.
81
+
73
82
  ## Accessibility
74
83
 
75
84
  - CommandMenuTrigger is a real <button> named by its visible placeholder, with aria-haspopup=dialog and aria-keyshortcuts (Meta+K on macOS, Control+K elsewhere). Its Kbd hint is aria-hidden, so the shortcut is announced once rather than read as part of the name.
@@ -0,0 +1,92 @@
1
+ # ConfirmButton
2
+
3
+ **Status:** stable
4
+
5
+ An inline two-step confirm for a low-stakes destructive or irreversible action: the first press arms it ("Delete" → "Confirm delete?", destructive), a second press within a few seconds runs it, and leaving it disarms it.
6
+
7
+ **Keywords:** confirm, confirmation, two-step, press again, click again, arm, delete, remove, destructive, inline confirm, hold to confirm
8
+
9
+ ## Usage
10
+
11
+ Use ConfirmButton when an action deserves one beat of second thought but not a modal: deleting a draft, removing a row, revoking one token, clearing a list that can be rebuilt. The question appears on the button the user is already pressing, so confirming costs one more press in the same place and backing out costs nothing.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Keep the idle label a plain verb ("Delete", "Remove", "Revoke") and let the confirm label ask about it ("Confirm delete?" by default, or `confirmLabel`). The user should read the consequence on the very control they are about to press again.
16
+ - Do: Leave room for it to grow. The button eases wider to fit the question, so give it space at the end of a row or in a table's action column rather than wedging it between fixed-width neighbours.
17
+ - Do: Return the promise from `onConfirm` when the action is async. The button stays armed with a spinner until it settles and refuses further presses, so a slow network cannot run it twice.
18
+ - Don't: Do not use it for high-stakes or wide-reaching actions — deleting a project, an account, anything other people depend on. Those need the consequence spelled out and a typed or deliberate confirmation; use DestructiveActionModal. → use **DestructiveActionModal**
19
+ - Don't: Do not use it where an undo is possible. If the action can be reversed, run it on the first press and offer Undo in a Toast — no question at all is faster than a good question. → use **Toast**
20
+ - Don't: Do not use it for a non-destructive primary action ("Save", "Send"). Asking twice for something harmless teaches people to press twice without reading, which is what defeats the confirm when it matters; use Button. → use **Button**
21
+
22
+ ## Props
23
+
24
+ | Name | Type | Default | Description |
25
+ | --- | --- | --- | --- |
26
+ | children | ReactNode | — | The idle label — the action, e.g. "Delete". |
27
+ | confirmLabel | ReactNode | "Confirm <label>?" | The label while armed. Defaults to "Confirm delete?" for a text label "Delete" (first letter lowered) and "Confirm?" otherwise. Pass it to localise. |
28
+ | onConfirm | () => unknown | — | Runs on the second press while armed. Return a promise to hold the button armed with a spinner until it settles. |
29
+ | timeout | number | 3000 | How long it waits for the second press, in ms. A decision time, so it is not scaled by motion settings. |
30
+ | variant | ButtonVariant | "secondary" | The idle look — any Button variant. |
31
+ | confirmVariant | ButtonVariant | "destructive" | The armed look — any Button variant. |
32
+ | size | "xs" \| "sm" \| "md" \| "cta" | "md" | A labelled Button size. Icon-only sizes are excluded: the label is the interaction. |
33
+ | icon | ReactNode | — | A leading icon kept through both states, e.g. `<Icon name="trash" />`. |
34
+ | disabled | boolean | false | Disables it; disabling while armed also disarms. |
35
+ | confirmHint | string | "Press again to confirm." | Read to assistive tech after the armed label when it arms. Override to localise. |
36
+ | 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 width easing and the draining countdown are the flourish; the label cross-fade and colour change stay. |
37
+
38
+ ## Variants
39
+
40
+ - **variant**: primary, secondary, ghost, outline, destructive, link, floating, field, on-color, on-color-outline, on-color-ghost
41
+ - **confirmVariant**: destructive, primary, secondary, outline
42
+ - **size**: xs, sm, md, cta
43
+
44
+ ## Tokens used
45
+
46
+ `secondary`, `secondary-foreground`, `destructive`, `destructive-foreground`, `ring`
47
+
48
+ ## Motion
49
+
50
+ - **arrival** · Root · _no motion_ — It is a button in a row of actions, and arrives with that row. Its moment is the arming, not the mount.
51
+ - **response** · Root · `motion-press` — Inherited from Button with motion-interactive. The press settle confirms the first press landed even before the label has finished changing — which is what stops a second, accidental press.
52
+ - **transition** · Root · `motion-size` — Arming changes the label, so the button's width changes. Easing it makes the button visibly grow into the question and its neighbours travel, instead of the row jumping under the pointer that is about to press again. useAutoSize measures the content and writes a px width for motion-size to ease between (a content-sized box is width:auto before and after, which cannot transition). The labels cross-fade in one cell so there is never a frame with both or neither.
53
+ - **activity** · Countdown · _custom (outside the recipe layer)_ — While armed, a thin line drains along the foot of the button over the timeout — the only way to see that the question expires. A transition on scale, timed by the `timeout` prop and multiplied by the motion scale and expressive factor, so under reduced motion or `subtle` it is absent rather than frozen full.
54
+
55
+ ## Accessibility
56
+
57
+ - A native <button> through Button. Focus stays on it through both presses; Enter and Space confirm exactly as a click does.
58
+ - Only the current label is in the accessible name — the other is aria-hidden — so the name changes to the question while armed.
59
+ - Arming is also announced through a polite role=status region outside the button ("Confirm delete? Press again to confirm."), because a focused button's name change is not read by every screen reader.
60
+ - Escape disarms and is consumed, so it does not also close the dialog or popover around it. Focus leaving, a press elsewhere, or the timeout disarm it too.
61
+ - A second press within 300ms of arming is ignored: a double-click is a habit, not a decision.
62
+ - Under reduced motion the label still changes and the colour still turns; only the width easing and countdown are dropped.
63
+
64
+ ## Related
65
+
66
+ - Components: Button, DestructiveActionModal, Toast, OverflowMenu
67
+
68
+ ## Examples
69
+
70
+ ### Delete a row
71
+
72
+ ```tsx
73
+ import { ConfirmButton, Icon } from "@sableui/react"
74
+
75
+ <ConfirmButton icon={<Icon name="trash" />} onConfirm={() => removeRow(id)}>Delete</ConfirmButton>
76
+ ```
77
+
78
+ ### Custom question, longer wait
79
+
80
+ ```tsx
81
+ import { ConfirmButton } from "@sableui/react"
82
+
83
+ <ConfirmButton variant="ghost" size="sm" confirmLabel="Revoke this key?" timeout={5000} onConfirm={revokeKey}>Revoke</ConfirmButton>
84
+ ```
85
+
86
+ ### Async confirm
87
+
88
+ ```tsx
89
+ import { ConfirmButton } from "@sableui/react"
90
+
91
+ <ConfirmButton onConfirm={() => api.deleteDraft(draft.id)}>Discard draft</ConfirmButton>
92
+ ```
@@ -50,6 +50,14 @@ Use DataTable when a data array and column definition should render a sortable,
50
50
 
51
51
  `muted-foreground`, `foreground`, `ring`
52
52
 
53
+ ## Motion
54
+
55
+ - **arrival** · Root · _no motion_ — Rows render with the page or the query that produced them. Staggering rows in delays reading a table someone opened to scan.
56
+ - **response** · Row · `motion-interactive` — Body rows ease to a muted wash under the pointer (TableRow's recipe), which keeps the eye on the right line across a wide row of numbers. Sortable headers are ghost Buttons and answer on Button's recipe.
57
+ - **transition** · Sort chevron · `motion-interactive` — One chevron turns to show direction instead of two glyphs swapping, so ascending to descending reads as the same column reversing. Its opacity rises from the resting 60% as the column becomes the sort.
58
+ - **transition** · Row · `motion-interactive` — A checked row's fill eases in across its cells, so selecting by checkbox reads as the row being picked rather than a band stamped over it.
59
+ - **activity** · Root · _no motion_ — DataTable has no loading state. While data is fetched, render Skeleton rows (motion-shimmer) in its place — only the caller knows a fetch is in flight.
60
+
53
61
  ## Accessibility
54
62
 
55
63
  - Sorted columns set aria-sort=ascending/descending on the header cell.
@@ -39,6 +39,13 @@ Use Entity to show a compact identity block for a person, organization, file, pr
39
39
 
40
40
  `accent`, `accent-foreground`, `foreground`, `muted-foreground`, `ring`
41
41
 
42
+ ## Motion
43
+
44
+ - **arrival** · Root · _no motion_ — Rows arrive with the list they belong to. Entering one at a time is a stagger, and a stagger on a list someone opened to scan is a delay.
45
+ - **response** · Root · `motion-interactive` — Only the interactive row reacts: its hover fill, a step off the surface it sits on, eases in. A static row never fakes clickability, and an interactive one answers the pointer without flicker as it moves down a list.
46
+ - **transition** · Root · `motion-interactive` — `selected` eases the fill onto the current row of a list–detail view, so opening another record reads as the selection moving rather than two rows flashing.
47
+ - **activity** · Root · _no motion_ — A record row has nothing of its own to wait for. A row whose action is pending puts a loading Button in `action`.
48
+
42
49
  ## Accessibility
43
50
 
44
51
  - Default rows are presentational layout; semantics come from the content you place in title/subtitle/action.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message and closes the popover.
5
+ A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message, shows a brief drawn-check thanks, and closes the popover.
6
6
 
7
7
  **Keywords:** feedback, sentiment, rating, prompt
8
8
 
@@ -26,11 +26,20 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
26
26
  | placeholder | string | "What can we improve?" | Placeholder for the message field. |
27
27
  | submitLabel | string | "Send" | Submit button text. |
28
28
  | ratings | FeedbackRating[] | 🙁 / 😐 / 🙂 | The rating options shown as a row of toggles. Each is { value, label }. |
29
- | onSubmit | (data: { rating?: string; message: string }) => void | — | Called with the selected rating + message when the form is submitted. |
29
+ | onSubmit | (data: { rating?: string; message: string }) => void | — | Called with the selected rating + message when the form is submitted. The thanks shows right away; send the report in the background. |
30
+ | successMessage | string | "Thanks for the feedback." | The line shown, and read out, for about 1.2s after sending, before the popover closes on its own. |
31
+ | 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 drawn check is the flourish; the thanks line and its dwell stay. |
30
32
 
31
33
  ## Tokens used
32
34
 
33
- `popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
35
+ `popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`
36
+
37
+ ## Motion
38
+
39
+ - **arrival** · Popover · `motion-pop` — The form opens from its trigger, so the eye goes from the button it pressed to the field it should type in. Popover owns the recipe.
40
+ - **response** · Rating toggles · `motion-interactive` — The rating row is a choice among a few similar marks. The eased selected fill is what makes the pick read as made, not just hovered.
41
+ - **transition** · Thanks · `motion-tick` — Sending is the one moment the person needs to know their words arrived. The form fades out in place and a check draws itself in over it, then the popover closes. Without it the popover just vanished, which looks the same as the form being dismissed or the message lost.
42
+ - **activity** · Root · _no motion_ — The thanks shows as soon as the form is sent; delivery is the app's background work. A spinner for a one-line report would make a quick, optional gesture feel heavy.
34
43
 
35
44
  ## Accessibility
36
45
 
@@ -39,6 +48,8 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
39
48
  - The form is named by its heading (aria-labelledby), so it is announced as "Send feedback" rather than an unnamed form.
40
49
  - The message field carries an aria-label matching its placeholder.
41
50
  - Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer.
51
+ - On send, focus moves to the thanks line (tabIndex=-1), which reads it out, and the form becomes inert so focus cannot land in invisible fields. The line is not also a live region, which read it twice. When the popover closes, Base UI returns focus to the trigger.
52
+ - The thanks dwell (about 1.2s) is a reading time, not motion, so it is the same under reduced motion.
42
53
 
43
54
  ## Related
44
55
 
@@ -61,3 +72,11 @@ import { Feedback } from "@sableui/react"
61
72
 
62
73
  <Feedback label="Tell us" submitLabel="Send it" placeholder="Describe the issue…" />
63
74
  ```
75
+
76
+ ### Custom thanks
77
+
78
+ ```tsx
79
+ import { Feedback } from "@sableui/react"
80
+
81
+ <Feedback successMessage="Got it — thank you." onSubmit={({ rating, message }) => report(rating, message)} />
82
+ ```
@@ -32,6 +32,13 @@ Use InputGroup when something belongs inside the field with the text: a unit, a
32
32
 
33
33
  `input`, `card`, `ring`, `destructive`, `muted-foreground`, `foreground`
34
34
 
35
+ ## Motion
36
+
37
+ - **arrival** · Root · _no motion_ — It arrives with the form it belongs to. A field that animated in on its own would be briefly unusable for no stated reason.
38
+ - **response** · Root · `motion-interactive` — The shell owns one focus ring for the input and its addons and eases it in, so focusing the text, or tabbing to a copy button inside the field, reads as the one field taking focus.
39
+ - **transition** · Root · `motion-interactive` — An invalid value turns the shell's edge destructive on the same ease, so the error lands as the field changing state rather than a red box swapping in.
40
+ - **activity** · Root · _no motion_ — A field does no work of its own. A pending check (an availability lookup) puts a Spinner in `trailing`, which is where its answer will appear.
41
+
35
42
  ## Accessibility
36
43
 
37
44
  - The Input stays the focusable control; the group draws the focus ring around the whole field while it is focused.
@@ -66,7 +73,7 @@ import { Field, Icon, Input, InputGroup } from "@sableui/react"
66
73
  ```tsx
67
74
  import { Button, Field, Icon, Input, InputGroup } from "@sableui/react"
68
75
 
69
- <Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><Icon name={copied ? "check" : "copy"} /></Button>}><Input readOnly value={key} /></InputGroup></Field>
76
+ <Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button>}><Input readOnly value={key} /></InputGroup></Field>
70
77
  ```
71
78
 
72
79
  ### Password with reveal
@@ -34,7 +34,8 @@ Use LineChart to compare how two or more measures change over the same sequence,
34
34
  | grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
35
35
  | xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
36
36
  | yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
37
- | animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. |
37
+ | 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 arrival (lines drawing on) and the data-change morph are the flourish; the gliding cursor, dots and tooltip are the feedback `subtle` keeps. |
38
+ | animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`. |
38
39
  | activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
39
40
  | defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
40
41
  | onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
@@ -49,9 +50,9 @@ Use LineChart to compare how two or more measures change over the same sequence,
49
50
 
50
51
  ## Motion
51
52
 
52
- - **arrival** · Line · _custom (outside the recipe layer)_ — Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade in. On the motion tokens; reduced motion shows the finished chart.
53
+ - **arrival** · Line · _custom (outside the recipe layer)_ — Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade 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
54
  - **response** · Cursor + tooltip · _no motion_ — Hover and focus move the cursor and tooltip instantly — a lagging cursor reads as a slow chart.
54
- - **transition** · Line + dots · _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.
55
+ - **transition** · Line + dots · _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.
55
56
  - **activity** · LineChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
56
57
 
57
58
  ## Accessibility
@@ -58,5 +58,5 @@ import { MiddleTruncate } from "@sableui/react"
58
58
  import { Button, Icon, MiddleTruncate, Row, useCopy } from "@sableui/react"
59
59
 
60
60
  const { copy, copied } = useCopy(commitHash)
61
- <Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><Icon name={copied ? "check" : "copy"} /></Button></Row>
61
+ <Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button><span role="status" className="sr-only">{copied ? "Copied" : ""}</span></Row>
62
62
  ```
@@ -37,6 +37,14 @@ Use Note for helpful supporting information that should sit near the relevant co
37
37
 
38
38
  `foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`
39
39
 
40
+ ## Motion
41
+
42
+ - **arrival** · Root · _no motion_ — A note is part of the content it annotates and renders with it. A callout that animated in would pull the eye before the reader has reached it in the flow.
43
+ - **response** · Action · `motion-interactive` — The action's focus ring eases in, and its underline thickens on hover. Inside a block of guidance, that is what separates the one thing to do from the prose around it.
44
+ - **response** · Action · `motion-press` — A note's action usually navigates or opens something; the small settle on press is the only confirmation before the next surface paints.
45
+ - **transition** · Root · _no motion_ — A note has no states to move between: its tone is fixed when it renders. A note that changes tone is a new message, and should arrive as one.
46
+ - **activity** · Root · _no motion_ — Static guidance. Work in progress belongs in a Progress or a Spinner, not in a callout.
47
+
40
48
  ## Accessibility
41
49
 
42
50
  - Rendered with role="note" so assistive tech can announce it as an aside.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- An unread indicator. Renders a bare dot or a count pill (capped as "{max}+"), or nothing when there's nothing to show. overlay pins it to the corner of a relative host; it is aria-hidden, so the host's name carries the count.
5
+ An unread indicator. Renders a bare dot or a count pill (capped as "{max}+"), or nothing when there's nothing to show. A changing count rolls to its new number, and a badge that appears after being empty pops in. overlay pins it to the corner of a relative host; it is aria-hidden, so the host's name carries the count.
6
6
 
7
7
  **Keywords:** notification, badge, count, unread
8
8
 
@@ -28,6 +28,7 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
28
28
  | tone | "destructive" \| "primary" \| "neutral" | "destructive" | Fill color. destructive for alarming counts; primary/neutral for non-alarming ones. |
29
29
  | size | "sm" \| "md" \| "lg" | "md" | Scales the dot/pill on the spacing scale (count pill: sm 16px, md 18px, lg 20px at the default density). |
30
30
  | overlay | boolean | false | Pin to the top-end corner of the nearest relative host (an icon button) — one placement for every caller instead of hand-tuned absolute offsets. |
31
+ | 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. Under `subtle` the count updates in place and the badge fades in without the pop. |
31
32
 
32
33
  ## Variants
33
34
 
@@ -39,6 +40,13 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
39
40
 
40
41
  `destructive`, `destructive-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`
41
42
 
43
+ ## Motion
44
+
45
+ - **arrival** · root · `motion-appear` — A badge that shows up after being empty is the news — something just arrived — so it fades and scales in where it sits instead of popping into existence. Never on first paint: a badge that mounts with a count is content, and a page of them popping on load is noise.
46
+ - **response** · root · _no motion_ — It is decorative and not interactive; the host it sits on answers the pointer.
47
+ - **transition** · Count · _custom (outside the recipe layer)_ — The count rolls to its new number through NumberTicker (expressive), so 3 → 4 reads as one more arriving rather than a different badge. Past the cap the ticker holds at max with a static "+", so a count climbing beyond 99 does not keep rolling a badge that has not changed.
48
+ - **activity** · root · _no motion_ — An unread count has no working state.
49
+
42
50
  ## Accessibility
43
51
 
44
52
  - The badge is aria-hidden: a count rendered on a control is not associated with it, so the host needs an accessible name that includes the count — "Notifications, 3 unread" — or a screen-reader user hears "Notifications, button" and never learns there is anything new. Pass aria-hidden={false} only for a standalone badge whose number is the information.
@@ -46,7 +54,7 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
46
54
 
47
55
  ## Related
48
56
 
49
- - Components: Badge, StatusDot
57
+ - Components: Badge, StatusDot, NumberTicker
50
58
 
51
59
  ## Examples
52
60
 
@@ -0,0 +1,86 @@
1
+ # NumberTicker
2
+
3
+ **Status:** stable
4
+
5
+ A number that rolls to its new value digit by digit, like an odometer. Formatted with Intl.NumberFormat (currency, percent, compact, any locale); separators, signs and units hold still while each digit column turns, and a digit that appears or goes away eases its width so nothing around it jumps. At rest on first paint; under subtle or reduced motion the value simply updates.
6
+
7
+ **Keywords:** number, counter, odometer, rolling number, animated number, ticker, count up, stat, kpi, metric, price
8
+
9
+ ## Usage
10
+
11
+ Use NumberTicker for a figure that changes in place while someone is looking at it — a dashboard total after a filter changes, a live count, a price as a plan's options change, a score. The roll says "this number moved, and by roughly this much"; the reader sees the change happen instead of noticing, later, that a number is different.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it for the headline figure that a control on the same screen changes — the total above a chart, the price beside a quantity stepper — so cause and effect are seen together.
16
+ - Do: Format through `format` (currency, percent, compact) rather than pre-formatting a string, so separators and units stay put while only the digits roll.
17
+ - Do: Pass `aria-live="polite"` only when the change itself is news the reader would otherwise miss; most tickers sit beside the control that changed them and need no announcement.
18
+ - Don't: Do not use it for values that change many times a second (a stopwatch, a stock tick, a progress percentage) — the rolls never land and read as a blur. Render the number as plain tabular text.
19
+ - Don't: Do not use it for numbers that are names rather than amounts — an order ID, a phone number, a year, a version. Rolling implies a quantity going up or down.
20
+ - Don't: Do not animate a number on page load to make it feel alive. The first paint is at rest on purpose; for an entrance, reveal the card it sits in (the Reveal motion primitive).
21
+ - Don't: Do not use it for a value inside a known range that needs a picture of how full it is — show a Meter, with the number beside it. → use **Meter**
22
+
23
+ ## Props
24
+
25
+ | Name | Type | Default | Description |
26
+ | --- | --- | --- | --- |
27
+ | value | number | — | The number to show. When it changes, the digits roll to it. |
28
+ | format | Intl.NumberFormatOptions | — | How to write the number — `{ style: "currency", currency: "USD" }`, `{ style: "percent" }`, `{ notation: "compact" }`, fraction digits. Passed to Intl.NumberFormat; an inline object is fine (it is compared by value). |
29
+ | locales | string \| string[] | runtime locale | BCP 47 locale(s) for the formatting. Digits roll through the locale's own numbering system. |
30
+ | prefix | ReactNode | — | Before the number. A string becomes part of the number's text (and rolls in or out with it); an element is rendered as given. |
31
+ | suffix | ReactNode | — | After the number ("+", " pts"). A string becomes part of the number's text; an element is rendered as given. |
32
+ | trend | "auto" \| "up" \| "down" | "auto" | Which way the digits roll. `auto` rolls up as the value grows and down as it shrinks; `up` / `down` fix it (a countdown, a score that only climbs). |
33
+ | 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 roll is expressive, so `subtle` updates the value in place. |
34
+ | className | string | — | Merged onto the root span. Set the type tier here (`text-heading-2`, `text-display-lg`); the ticker adds tabular figures so widths hold still. |
35
+ | ...span props | Omit<ComponentPropsWithRef<"span">, "children" \| "prefix"> | — | Remaining span attributes pass through to the root — `aria-live` to announce changes, `id`, data attributes. |
36
+
37
+ ## Motion
38
+
39
+ - **arrival** · Root · _no motion_ — The first paint is at rest. A number counting up on page load is decoration that delays the reading; the roll is kept for the moment the value actually changes.
40
+ - **response** · Root · _no motion_ — It is a reading, not a control; the control that changes it gives the feedback.
41
+ - **transition** · Digit · _custom (outside the recipe layer)_ — Each digit column turns to its new place on the expressive roll (the slower token, emphasized easing, staggered from the ones column), and arriving or departing columns ease their width. The roll is what shows a change happened and how big it was — the hundreds turning reads as a bigger move than the ones. No recipe can translate per-digit columns, so it is custom; it is multiplied by --motion-expressive, so `subtle` and reduced motion land the value at once.
42
+ - **activity** · Root · _no motion_ — A number has no working state; show a Skeleton while it loads.
43
+
44
+ ## Accessibility
45
+
46
+ - The whole written number (with any string prefix or suffix) is real text in a visually hidden span; the rolling columns are aria-hidden and draw their glyphs as generated content, so screen readers, find-in-page and copy all get "$1,234.50", never a stack of digits.
47
+ - It is not a live region by default — a figure that changes because the reader moved a control is already where their attention is. Pass `aria-live="polite"` when a change elsewhere should be announced.
48
+ - Tabular figures keep every digit the same width, so a roll never shifts the text around it.
49
+
50
+ ## Related
51
+
52
+ - Components: Meter, NotificationBadge, PieChart, RadialChart, Sparkline
53
+
54
+ ## Examples
55
+
56
+ ### Basic
57
+
58
+ ```tsx
59
+ import { NumberTicker } from "@sableui/react"
60
+
61
+ <NumberTicker value={1284} className="text-heading-2" />
62
+ ```
63
+
64
+ ### Currency
65
+
66
+ ```tsx
67
+ import { NumberTicker } from "@sableui/react"
68
+
69
+ <NumberTicker value={4280.5} format={{ style: "currency", currency: "USD" }} className="text-display-lg" />
70
+ ```
71
+
72
+ ### Percent, announced
73
+
74
+ ```tsx
75
+ import { NumberTicker } from "@sableui/react"
76
+
77
+ <NumberTicker value={0.428} format={{ style: "percent", maximumFractionDigits: 1 }} aria-live="polite" />
78
+ ```
79
+
80
+ ### Subtle
81
+
82
+ ```tsx
83
+ import { NumberTicker } from "@sableui/react"
84
+
85
+ <NumberTicker value={92} suffix=" pts" motion="subtle" />
86
+ ```