@sableui/react 0.13.2 → 0.15.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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +172 -0
- package/CONTRIBUTING.md +6 -4
- package/DESIGN.md +10 -3
- package/dist/blocks/approval-block.js +93 -0
- package/dist/blocks/approval-block.meta.js +23 -0
- package/dist/blocks/index.d.ts +27 -1
- package/dist/blocks/index.js +8 -0
- package/dist/blocks/message-reactions-block.js +87 -0
- package/dist/blocks/message-reactions-block.meta.js +23 -0
- package/dist/components/accordion/accordion.meta.js +8 -2
- package/dist/components/area-chart/area-chart.meta.js +10 -3
- package/dist/components/attachment/attachment.meta.js +26 -0
- package/dist/components/bar-chart/bar-chart.meta.js +11 -4
- package/dist/components/bottom-nav/bottom-nav.js +3 -1
- package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
- package/dist/components/bubble/bubble.js +105 -5
- package/dist/components/bubble/bubble.meta.js +42 -6
- package/dist/components/bubble/bubble.variants.js +1 -1
- package/dist/components/bubble/index.js +2 -0
- package/dist/components/button/button.js +21 -4
- package/dist/components/button/button.meta.js +7 -7
- package/dist/components/calendar/calendar.js +18 -1
- package/dist/components/calendar/calendar.meta.js +38 -0
- package/dist/components/carousel/carousel.js +6 -4
- package/dist/components/carousel/carousel.meta.js +42 -0
- package/dist/components/chat-composer/chat-composer.js +21 -21
- package/dist/components/chat-composer/chat-composer.meta.js +38 -0
- package/dist/components/chat-composer/use-eased-growth.js +70 -0
- package/dist/components/checkbox/checkbox-mark.js +8 -1
- package/dist/components/checkbox/checkbox.js +3 -1
- package/dist/components/checkbox/checkbox.meta.js +32 -0
- package/dist/components/checkbox-card/checkbox-card.js +3 -0
- package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
- package/dist/components/code-block/code-block.js +14 -3
- package/dist/components/code-block/code-block.meta.js +28 -1
- package/dist/components/collapsible/collapsible.meta.js +32 -0
- package/dist/components/color-field/color-field.meta.js +26 -0
- package/dist/components/combobox/combobox.meta.js +44 -0
- package/dist/components/command-menu/command-menu.js +8 -3
- package/dist/components/command-menu/command-menu.meta.js +38 -0
- package/dist/components/confirm-button/confirm-button.js +179 -0
- package/dist/components/confirm-button/confirm-button.meta.js +143 -0
- package/dist/components/confirm-button/index.js +7 -0
- package/dist/components/data-table/data-table.js +6 -2
- package/dist/components/data-table/data-table.meta.js +32 -0
- package/dist/components/entity/entity.meta.js +26 -0
- package/dist/components/feedback/feedback.js +120 -47
- package/dist/components/feedback/feedback.meta.js +55 -4
- package/dist/components/input-group/input-group.meta.js +27 -1
- package/dist/components/like-button/index.js +7 -0
- package/dist/components/like-button/like-button.js +89 -0
- package/dist/components/like-button/like-button.meta.js +132 -0
- package/dist/components/line-chart/line-chart.meta.js +10 -3
- package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
- package/dist/components/note/note.meta.js +32 -0
- package/dist/components/notification-badge/notification-badge.js +17 -5
- package/dist/components/notification-badge/notification-badge.meta.js +34 -2
- package/dist/components/number-ticker/index.js +7 -0
- package/dist/components/number-ticker/number-ticker.js +246 -0
- package/dist/components/number-ticker/number-ticker.meta.js +159 -0
- package/dist/components/otp-field/otp-field.js +23 -3
- package/dist/components/otp-field/otp-field.meta.js +23 -2
- package/dist/components/panel-group/panel-group.meta.js +26 -0
- package/dist/components/panel-section/panel-section.js +37 -10
- package/dist/components/panel-section/panel-section.meta.js +32 -0
- package/dist/components/pie-chart/pie-chart.js +20 -11
- package/dist/components/pie-chart/pie-chart.meta.js +17 -4
- package/dist/components/pill/pill.meta.js +32 -0
- package/dist/components/radar-chart/radar-chart.js +4 -3
- package/dist/components/radar-chart/radar-chart.meta.js +10 -3
- package/dist/components/radial-chart/radial-chart.js +22 -22
- package/dist/components/radial-chart/radial-chart.meta.js +17 -4
- package/dist/components/radio/radio.meta.js +26 -0
- package/dist/components/radio-card/radio-card.meta.js +38 -0
- package/dist/components/reaction-picker/index.js +13 -0
- package/dist/components/reaction-picker/reaction-picker.js +104 -0
- package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
- package/dist/components/relative-time/relative-time.meta.js +32 -0
- package/dist/components/scroller/scroller.meta.js +32 -0
- package/dist/components/scrub-field/scrub-field.meta.js +32 -0
- package/dist/components/search-input/search-input.js +9 -7
- package/dist/components/search-input/search-input.meta.js +26 -0
- package/dist/components/show-more/show-more.js +1 -4
- package/dist/components/show-more/show-more.meta.js +38 -0
- package/dist/components/side-nav/side-nav.js +21 -1
- package/dist/components/side-nav/side-nav.meta.js +32 -0
- package/dist/components/side-nav/side-nav.variants.js +3 -1
- package/dist/components/sidebar/sidebar.js +31 -8
- package/dist/components/sidebar/sidebar.meta.js +38 -0
- package/dist/components/slider/slider.meta.js +26 -0
- package/dist/components/sparkline/sparkline.js +7 -4
- package/dist/components/sparkline/sparkline.meta.js +9 -2
- package/dist/components/table/table.meta.js +26 -0
- package/dist/components/tabs/tabs.js +27 -8
- package/dist/components/tabs/tabs.meta.js +46 -3
- package/dist/components/theme-selector/theme-selector.meta.js +32 -0
- package/dist/components/theme-switcher/theme-switcher.js +4 -1
- package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
- package/dist/components/toast/toast.js +25 -13
- package/dist/components/toast/toast.meta.js +7 -1
- package/dist/components/toggle/toggle.js +28 -5
- package/dist/components/toggle/toggle.meta.js +44 -2
- package/dist/components/tool-call/index.js +8 -0
- package/dist/components/tool-call/tool-call.js +117 -0
- package/dist/components/tool-call/tool-call.meta.js +147 -0
- package/dist/components/tool-call/tool-call.variants.js +33 -0
- package/dist/components/tooltip/index.js +3 -1
- package/dist/components/tooltip/tooltip.js +84 -0
- package/dist/components/tooltip/tooltip.meta.js +16 -1
- package/dist/components/tree/tree.js +71 -6
- package/dist/components/tree/tree.meta.js +32 -0
- package/dist/components/video/video.meta.js +32 -0
- package/dist/file-hashes.json +130 -104
- package/dist/foundations/motion.js +69 -3
- package/dist/icon-sets/reicon.js +11 -1
- package/dist/index.d.ts +907 -104
- package/dist/index.js +6 -1
- package/dist/lib/chart/cartesian.js +9 -10
- package/dist/lib/chart/chart-center.js +30 -0
- package/dist/lib/chart/chart-frame.js +15 -8
- package/dist/lib/chart/dim.js +13 -0
- package/dist/lib/chart/format.js +7 -1
- package/dist/lib/chart/motion.js +6 -4
- package/dist/lib/highlight-glide.js +68 -10
- package/dist/lib/taxonomy.js +5 -0
- package/dist/motion/Burst.js +79 -0
- package/dist/motion/MotionProvider.js +45 -3
- package/dist/motion/Reveal.js +1 -1
- package/dist/motion/TextReveal.js +1 -1
- package/dist/motion/index.js +17 -2
- package/dist/motion/useAutoSize.js +42 -0
- package/dist/motion/useShake.js +25 -0
- package/dist/styles.css +439 -5
- package/llms/blocks/approval-block.md +128 -0
- package/llms/blocks/message-reactions-block.md +116 -0
- package/llms/components/accordion.md +3 -2
- package/llms/components/area-chart.md +4 -3
- package/llms/components/attachment.md +7 -0
- package/llms/components/bar-chart.md +5 -4
- package/llms/components/bottom-nav.md +8 -0
- package/llms/components/breadcrumbs.md +7 -0
- package/llms/components/bubble.md +17 -7
- package/llms/components/button.md +6 -6
- package/llms/components/calendar.md +9 -0
- package/llms/components/carousel.md +17 -0
- package/llms/components/chat-composer.md +9 -0
- package/llms/components/checkbox-card.md +8 -0
- package/llms/components/checkbox.md +8 -0
- package/llms/components/code-block.md +8 -0
- package/llms/components/collapsible.md +8 -0
- package/llms/components/color-field.md +7 -0
- package/llms/components/combobox.md +10 -0
- package/llms/components/command-menu.md +9 -0
- package/llms/components/confirm-button.md +92 -0
- package/llms/components/data-table.md +8 -0
- package/llms/components/entity.md +7 -0
- package/llms/components/feedback.md +23 -3
- package/llms/components/input-group.md +8 -1
- package/llms/components/like-button.md +91 -0
- package/llms/components/line-chart.md +4 -3
- package/llms/components/middle-truncate.md +1 -1
- package/llms/components/note.md +8 -0
- package/llms/components/notification-badge.md +10 -2
- package/llms/components/number-ticker.md +86 -0
- package/llms/components/otp-field.md +15 -1
- package/llms/components/panel-group.md +7 -0
- package/llms/components/panel-section.md +8 -0
- package/llms/components/pie-chart.md +6 -4
- package/llms/components/pill.md +8 -0
- package/llms/components/radar-chart.md +4 -3
- package/llms/components/radial-chart.md +6 -4
- package/llms/components/radio-card.md +9 -0
- package/llms/components/radio.md +7 -0
- package/llms/components/reaction-picker.md +115 -0
- package/llms/components/relative-time.md +8 -0
- package/llms/components/scroller.md +8 -0
- package/llms/components/scrub-field.md +8 -0
- package/llms/components/search-input.md +7 -0
- package/llms/components/show-more.md +9 -0
- package/llms/components/side-nav.md +8 -0
- package/llms/components/sidebar.md +9 -0
- package/llms/components/slider.md +7 -0
- package/llms/components/sparkline.md +3 -2
- package/llms/components/table.md +7 -0
- package/llms/components/tabs.md +20 -2
- package/llms/components/theme-selector.md +8 -0
- package/llms/components/theme-switcher.md +17 -0
- package/llms/components/toast.md +2 -1
- package/llms/components/toggle.md +19 -2
- package/llms/components/tool-call.md +115 -0
- package/llms/components/tooltip.md +11 -1
- package/llms/components/tree.md +8 -0
- package/llms/components/video.md +8 -0
- package/llms/foundations/motion.md +64 -3
- package/llms/reference/utilities.md +26 -1
- package/llms/tasks/reviewing-motion.md +14 -0
- package/llms-full.txt +1337 -69
- package/llms.txt +19 -11
- package/package.json +1 -1
- package/registry.json +3040 -124
- package/targets/ios.json +45 -0
package/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sable (@sableui/react)
|
|
2
2
|
|
|
3
|
-
> An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter.
|
|
3
|
+
> An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter. 121 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
|
|
4
4
|
|
|
5
5
|
## Start here — brief, composition, evidence
|
|
6
6
|
|
|
@@ -3080,6 +3080,20 @@ and canceled work does not report success. Inspect the behavior at normal speed:
|
|
|
3080
3080
|
is the response immediate enough for its frequency, and does the movement explain
|
|
3081
3081
|
the change without delaying access to content?
|
|
3082
3082
|
|
|
3083
|
+
Check continuity before timing. When a selection, value or mark changes in
|
|
3084
|
+
place, one element should travel (an indicator glides, digits roll, a tick
|
|
3085
|
+
draws) rather than one thing switching off and another on. A snap between two
|
|
3086
|
+
states of the same control — a chevron that jumps, a fill that blinks from one
|
|
3087
|
+
tab to the next, an icon replaced between frames — is a finding even when every
|
|
3088
|
+
duration is a token. So is a story left unfinished: a refusal with no shake or
|
|
3089
|
+
message, a success with no confirmation where it happened.
|
|
3090
|
+
|
|
3091
|
+
Run it three ways, not two. Under `data-motion="subtle"` (or the component's
|
|
3092
|
+
`motion="subtle"`, or `<MotionScope level="subtle">`) feedback, overlays,
|
|
3093
|
+
disclosure and loaders should still play while glides, rolls, draws, shakes,
|
|
3094
|
+
staggers and overshoot resolve instantly; a flourish still moving there was
|
|
3095
|
+
built on a plain token instead of an expressive one.
|
|
3096
|
+
|
|
3083
3097
|
Repeat relevant behavior with reduced motion enabled. This should preserve state,
|
|
3084
3098
|
focus, status, and controls; hiding the animated content is not an equivalent
|
|
3085
3099
|
result. Test a custom driver directly—Sable's policy cannot disable an unrelated
|
|
@@ -3835,9 +3849,9 @@ The menu reads as above the dialog without any prop.
|
|
|
3835
3849
|
|
|
3836
3850
|
# Motion
|
|
3837
3851
|
|
|
3838
|
-
Motion explains a change: where something came from, what responded, and that work is happening.
|
|
3852
|
+
Motion explains a change: where something came from, what responded, and that work is happening. Components ship crafted by default: one indicator travels instead of two blinking, numbers roll, ticks draw, a refused answer shakes. It is built from named recipes, comes in two levels, and one switch turns it off.
|
|
3839
3853
|
|
|
3840
|
-
**4 slots · 3 speeds · enter-only overshoot · 1 kill switch**
|
|
3854
|
+
**4 slots · 3 speeds · 2 levels · enter-only overshoot · 1 kill switch**
|
|
3841
3855
|
|
|
3842
3856
|
## Three speeds
|
|
3843
3857
|
|
|
@@ -3861,7 +3875,15 @@ Reach for one named class before writing a transition.
|
|
|
3861
3875
|
| `motion-pop` | enter base · spring | Anchored or centred surfaces: popovers, menus, dialogs. |
|
|
3862
3876
|
| `motion-slide-{up,down,left,right}` | enter base / exit fast | Directional surfaces: sheets, toasts. |
|
|
3863
3877
|
| `motion-disclosure` | base · standard | Height changes: accordion, collapsible. |
|
|
3864
|
-
| `motion-icon-swap` | enter · emphasized cross-fade | Two icons of one control trading places: play/pause, outline/fill. |
|
|
3878
|
+
| `motion-icon-swap` | enter · emphasized cross-fade | Two icons of one control trading places: play/pause, outline/fill, copy/check. |
|
|
3879
|
+
| `motion-glide` | slow · emphasized · expressive | ONE indicator travelling to the selected option: tabs, segmented thumb, active dot. |
|
|
3880
|
+
| `motion-shake` | ~400ms decaying · expressive | A refused answer: wrong code, invalid submit. Plays while data-shake is set (useShake). Never the only signal. |
|
|
3881
|
+
| `motion-tick` | fast fade + base wipe · expressive | A check mark that draws itself in. Exits only fade. |
|
|
3882
|
+
| `motion-appear` | fast fade + base scale | A small control mounting in place: a clear button, a send arrow, a badge. @starting-style, no JS. |
|
|
3883
|
+
| `motion-arrive` | base · decelerate · staggered · expressive | Items a user action brings into a list: a folder's children on expand, a row just added. Never on every render. |
|
|
3884
|
+
| `motion-size` | base · emphasized · expressive | An element easing between intrinsic sizes when its label changes: a two-step confirm, Copy → Copied. |
|
|
3885
|
+
| `motion-bloom` | slower · 0 → 1.2 → 1 · expressive | A small glyph landing in its on state: a liked heart, a picked rating, a toast's status icon. Remount it (key) to replay; never on the resting state. |
|
|
3886
|
+
| `<Burst>` | slower ring + thrown dots · expressive | The follow-through behind a bloom, in the theme's palette families. Only on a choice a person made: a like, a star, a finished task. |
|
|
3865
3887
|
| `motion-draw` | slower · decelerate | SVG line art drawing on when a <Reveal> scrolls in. The path needs pathLength="1"; stagger siblings with --motion-draw-delay. |
|
|
3866
3888
|
| `motion-spin · pulse · shimmer · dot-bounce · ping` | loops | Activity. Scale-aware, so reduced motion freezes them. |
|
|
3867
3889
|
|
|
@@ -3875,6 +3897,19 @@ Reach for one named class before writing a transition.
|
|
|
3875
3897
|
| `ease-spring` | ~4% overshoot | A gentle pop on enter. Never on exit. |
|
|
3876
3898
|
| `ease-bounce` | ~10% overshoot | A playful pop, opt-in only. Enter only, never on exit. |
|
|
3877
3899
|
|
|
3900
|
+
## Two levels
|
|
3901
|
+
|
|
3902
|
+
Essential motion answers the user; expressive motion narrates continuity. Expressive durations are also multiplied by --motion-expressive, which `subtle` sets to 0. Each level can only turn motion down from what surrounds it.
|
|
3903
|
+
|
|
3904
|
+
| Name | Value | Use |
|
|
3905
|
+
|---|---|---|
|
|
3906
|
+
| `essential` | always, unless off | Colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover. |
|
|
3907
|
+
| `expressive` | full only | Glides, rolling numbers (NumberTicker), drawn ticks, blooms and bursts, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. |
|
|
3908
|
+
| `full` | default | Both levels play. The crafted default. |
|
|
3909
|
+
| `subtle` | data-motion="subtle" | Essential only; nothing overshoots. For dense tools, embedded widgets, readers who want calm without stillness. |
|
|
3910
|
+
| `off` | data-motion="off" · reduce-motion | Nothing moves; every state still changes. |
|
|
3911
|
+
| `where` | provider · scope · prop | <MotionProvider policy> for the app, <MotionScope level> for a region, `motion` on a component that owns a flourish. |
|
|
3912
|
+
|
|
3878
3913
|
## The four slots
|
|
3879
3914
|
|
|
3880
3915
|
Every interactive component answers all four, by filling the slot or declining it with a reason in meta.motion.
|
|
@@ -3895,6 +3930,13 @@ Every interactive component answers all four, by filling the slot or declining i
|
|
|
3895
3930
|
5. **Durations come from tokens, so --motion-scale can stop them.** prefers-reduced-motion and <MotionProvider> off both set --motion-scale to 0. A raw duration ignores that. *Enforced by sable-lint: raw-duration, builtin-animate.*
|
|
3896
3931
|
6. **Interactive elements always respond. Something styled for hover with no motion recipe snaps.** A snap between states reads as broken. The four prohibitive rules could not catch missing motion, so this one does. *Enforced by sable-lint: static-interactive.*
|
|
3897
3932
|
7. **JS motion reads useMotion().enabled and does nothing when it is false.** Reduced motion is an accessibility setting, not a style preference.
|
|
3933
|
+
8. **One thing travels; two things do not blink. When a selection, a value or a mark changes in place, move the one element that represents it.** A fill switching off here and on there is two events the eye has to connect. A glide, a roll or a draw is one, and it is most of what makes an interface feel made rather than assembled.
|
|
3934
|
+
9. **Flourishes are expressive: multiply their durations by --motion-expressive and gate JS ones on useMotion().expressive.** `subtle` promises the answers without the choreography. A glide on a plain token keeps playing after someone asked for calm.
|
|
3935
|
+
10. **A state change finishes its story: refused shakes and says why, success confirms in place, armed asks again.** Motion that starts a change and leaves the outcome to a snap reads as unfinished. ConfirmButton, OtpField's invalid shake and the copy-to-check swap are the reference cases.
|
|
3936
|
+
11. **Amplitude scales with smallness. A 16px glyph that turns on grows from nothing past its size; a surface travels from 0.96 and a press settles to 0.97.** The same 4% that is right for a dialog is invisible on an icon, which is why a state change on a small control used to read as a colour swap with no life in it. A dialog that overshot 20% would lurch. Size, not importance, sets how far a thing moves.
|
|
3937
|
+
12. **Stage a moment: the main object lands first, the follow-through after it.** A ring that opens, a glyph that blooms 90ms later, dots thrown 60ms after that read as one event with a cause and an effect. Everything starting on the same frame reads as a flash. LikeButton and the toast's status glyph are the reference cases.
|
|
3938
|
+
13. **Celebrate only what a person chose, and only at full motion.** A burst is a reply. On a like it is delight; on a system event, on load, or on every row of a list it is a notification wearing confetti. Under subtle the fill alone answers.
|
|
3939
|
+
14. **Component motion props and MotionScope only turn motion down.** `full` on a component means inherit. Nothing below the provider can switch motion back on for a reader who asked for it off.
|
|
3898
3940
|
|
|
3899
3941
|
## Never
|
|
3900
3942
|
|
|
@@ -3917,6 +3959,39 @@ Every interactive component answers all four, by filling the slot or declining i
|
|
|
3917
3959
|
|
|
3918
3960
|
Same feel, but reduced motion now resolves it instantly.
|
|
3919
3961
|
|
|
3962
|
+
### A selection that travels
|
|
3963
|
+
|
|
3964
|
+
```tsx
|
|
3965
|
+
// before
|
|
3966
|
+
<Tab className="data-[active]:border-b-2 data-[active]:border-foreground" />
|
|
3967
|
+
// after
|
|
3968
|
+
<Tabs.Indicator className="motion-glide left-(--active-tab-left) w-(--active-tab-width)" />
|
|
3969
|
+
```
|
|
3970
|
+
|
|
3971
|
+
The underline used to vanish under one tab and appear under the next. Now it slides, and Tabs renders it for you.
|
|
3972
|
+
|
|
3973
|
+
### A value that rolls
|
|
3974
|
+
|
|
3975
|
+
```tsx
|
|
3976
|
+
// before
|
|
3977
|
+
<span>{total}</span>
|
|
3978
|
+
// after
|
|
3979
|
+
<NumberTicker value={total} format={{ style: "currency", currency: "USD" }} />
|
|
3980
|
+
```
|
|
3981
|
+
|
|
3982
|
+
The digits that changed roll; the ones that did not stay still, so the eye goes straight to the difference.
|
|
3983
|
+
|
|
3984
|
+
### A glyph that lands
|
|
3985
|
+
|
|
3986
|
+
```tsx
|
|
3987
|
+
// before
|
|
3988
|
+
<Icon name="heart" variant={liked ? "solid" : "regular"} />
|
|
3989
|
+
// after
|
|
3990
|
+
<LikeButton count={likes} defaultPressed={liked} />
|
|
3991
|
+
```
|
|
3992
|
+
|
|
3993
|
+
The heart used to swap fill in one frame. Now it blooms past its size and settles while a ring and dots burst from it, and the count rolls: the same state, told as an event.
|
|
3994
|
+
|
|
3920
3995
|
### Faster exits
|
|
3921
3996
|
|
|
3922
3997
|
```tsx
|
|
@@ -7015,6 +7090,137 @@ export function ApiKeysBlock({ className, framed = true, onCreate, onKeyAction }
|
|
|
7015
7090
|
|
|
7016
7091
|
---
|
|
7017
7092
|
|
|
7093
|
+
# ApprovalBlock
|
|
7094
|
+
|
|
7095
|
+
An agent asking permission for one tool call: the question, what will happen and what can't be undone, the call's details, an "allow for the rest of this chat" Checkbox, and Deny / Allow.
|
|
7096
|
+
|
|
7097
|
+
**Intent:** Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing. The call itself, before and after the decision, is a ToolCall with status="awaiting", then "complete" or "denied". A destructive action outside an agent is DestructiveActionModal.
|
|
7098
|
+
|
|
7099
|
+
**Block:** AI & agents · card · designed for 18rem–28rem wide (fills its container up to the max)
|
|
7100
|
+
|
|
7101
|
+
## Notes
|
|
7102
|
+
|
|
7103
|
+
- The title is the question in plain words ("Send the invoice to Ada?"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.
|
|
7104
|
+
- The description says what can't be undone. That sentence is what the person is deciding on.
|
|
7105
|
+
- Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.
|
|
7106
|
+
- After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer.
|
|
7107
|
+
|
|
7108
|
+
## Accessibility
|
|
7109
|
+
|
|
7110
|
+
- Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.
|
|
7111
|
+
- The details are a dl, so each value is read with its label.
|
|
7112
|
+
- The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name.
|
|
7113
|
+
|
|
7114
|
+
## Source
|
|
7115
|
+
|
|
7116
|
+
```tsx
|
|
7117
|
+
import { useState, useEffect, useRef, useId } from "react"
|
|
7118
|
+
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, cn, Code, InlineStatus, Label } from "@sableui/react"
|
|
7119
|
+
|
|
7120
|
+
const request = {
|
|
7121
|
+
tool: "send_email",
|
|
7122
|
+
title: "Send the invoice to Ada?",
|
|
7123
|
+
description: "The agent wants to email invoice INV-1042. An email can't be unsent.",
|
|
7124
|
+
details: [
|
|
7125
|
+
{ label: "To", value: "ada@example.com" },
|
|
7126
|
+
{ label: "Subject", value: "INV-1042 for October" },
|
|
7127
|
+
{ label: "Attached", value: "INV-1042.pdf" },
|
|
7128
|
+
],
|
|
7129
|
+
allowed: "Allowed. Sending the email.",
|
|
7130
|
+
denied: "Denied. The agent won't send it.",
|
|
7131
|
+
}
|
|
7132
|
+
|
|
7133
|
+
export interface ApprovalBlockProps {
|
|
7134
|
+
className?: string
|
|
7135
|
+
/** false drops the card's surface and border — for a chat column that already frames it. */
|
|
7136
|
+
framed?: boolean
|
|
7137
|
+
/** The person allowed the call. `remember` is the "for the rest of this chat" choice. */
|
|
7138
|
+
onApprove?: (options: { remember: boolean }) => void
|
|
7139
|
+
/** The person declined the call. */
|
|
7140
|
+
onDeny?: () => void
|
|
7141
|
+
}
|
|
7142
|
+
|
|
7143
|
+
export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {
|
|
7144
|
+
const [decision, setDecision] = useState<"allowed" | "denied" | null>(null)
|
|
7145
|
+
const [remember, setRemember] = useState(false)
|
|
7146
|
+
const id = useId()
|
|
7147
|
+
const outcome = useRef<HTMLDivElement>(null)
|
|
7148
|
+
// The buttons leave with the decision, so focus moves to the line that
|
|
7149
|
+
// replaces them instead of falling back to the top of the page.
|
|
7150
|
+
useEffect(() => {
|
|
7151
|
+
if (decision) outcome.current?.focus()
|
|
7152
|
+
}, [decision])
|
|
7153
|
+
|
|
7154
|
+
return (
|
|
7155
|
+
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-md", className)}>
|
|
7156
|
+
<CardHeader>
|
|
7157
|
+
<CardTitle>{request.title}</CardTitle>
|
|
7158
|
+
<CardDescription>{request.description}</CardDescription>
|
|
7159
|
+
</CardHeader>
|
|
7160
|
+
<CardContent className="flex flex-col gap-4">
|
|
7161
|
+
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm">
|
|
7162
|
+
{request.details.map((d) => (
|
|
7163
|
+
<div key={d.label} className="contents">
|
|
7164
|
+
<dt className="text-muted-foreground">{d.label}</dt>
|
|
7165
|
+
<dd className="truncate text-foreground">{d.value}</dd>
|
|
7166
|
+
</div>
|
|
7167
|
+
))}
|
|
7168
|
+
<div className="contents">
|
|
7169
|
+
<dt className="text-muted-foreground">Tool</dt>
|
|
7170
|
+
<dd className="min-w-0">
|
|
7171
|
+
<Code>{request.tool}</Code>
|
|
7172
|
+
</dd>
|
|
7173
|
+
</div>
|
|
7174
|
+
</dl>
|
|
7175
|
+
{decision ? null : (
|
|
7176
|
+
<div className="flex items-center gap-2">
|
|
7177
|
+
<Checkbox id={`${id}-remember`} checked={remember} onCheckedChange={(v: boolean) => setRemember(v)} />
|
|
7178
|
+
<Label htmlFor={`${id}-remember`} className="text-body-sm">
|
|
7179
|
+
Allow <Code>{request.tool}</Code> for the rest of this chat
|
|
7180
|
+
</Label>
|
|
7181
|
+
</div>
|
|
7182
|
+
)}
|
|
7183
|
+
</CardContent>
|
|
7184
|
+
<CardFooter actions className="justify-end">
|
|
7185
|
+
{decision ? (
|
|
7186
|
+
<InlineStatus
|
|
7187
|
+
ref={outcome}
|
|
7188
|
+
tabIndex={-1}
|
|
7189
|
+
role="status"
|
|
7190
|
+
tone={decision === "allowed" ? "success" : "neutral"}
|
|
7191
|
+
className="me-auto outline-none"
|
|
7192
|
+
>
|
|
7193
|
+
{decision === "allowed" ? request.allowed : request.denied}
|
|
7194
|
+
</InlineStatus>
|
|
7195
|
+
) : (
|
|
7196
|
+
<>
|
|
7197
|
+
<Button
|
|
7198
|
+
variant="outline"
|
|
7199
|
+
onClick={() => {
|
|
7200
|
+
setDecision("denied")
|
|
7201
|
+
onDeny?.()
|
|
7202
|
+
}}
|
|
7203
|
+
>
|
|
7204
|
+
Deny
|
|
7205
|
+
</Button>
|
|
7206
|
+
<Button
|
|
7207
|
+
onClick={() => {
|
|
7208
|
+
setDecision("allowed")
|
|
7209
|
+
onApprove?.({ remember })
|
|
7210
|
+
}}
|
|
7211
|
+
>
|
|
7212
|
+
Allow
|
|
7213
|
+
</Button>
|
|
7214
|
+
</>
|
|
7215
|
+
)}
|
|
7216
|
+
</CardFooter>
|
|
7217
|
+
</Card>
|
|
7218
|
+
)
|
|
7219
|
+
}
|
|
7220
|
+
```
|
|
7221
|
+
|
|
7222
|
+
---
|
|
7223
|
+
|
|
7018
7224
|
# ChatThreadBlock
|
|
7019
7225
|
|
|
7020
7226
|
A short message thread on a card: Message rows with incoming muted Bubbles on the start side and your own primary Bubbles on the end.
|
|
@@ -8099,6 +8305,125 @@ export function MediaPlayerBlock({ className, framed = true, onPlayingChange, on
|
|
|
8099
8305
|
|
|
8100
8306
|
---
|
|
8101
8307
|
|
|
8308
|
+
# MessageReactionsBlock
|
|
8309
|
+
|
|
8310
|
+
A short thread you can react to: right-click or long-press a bubble for a menu with a row of quick reactions above Reply and Copy, or use the add-reaction pill under a message. A new reaction blooms in and sprays copies of its emoji.
|
|
8311
|
+
|
|
8312
|
+
**Intent:** Reach for this when a conversation excerpt should be reactable — a team thread, a comment card, a support transcript. A read-only excerpt is ChatThreadBlock; a working chat with a composer and history is the ChatPanel page shell.
|
|
8313
|
+
|
|
8314
|
+
**Block:** Social & messaging · card · designed for 18rem–24rem wide (fills its container up to the max)
|
|
8315
|
+
|
|
8316
|
+
## Notes
|
|
8317
|
+
|
|
8318
|
+
- Two ways in, on purpose: the right-click / long-press menu is fast once you know it is there, and the add-reaction pill is how a first-time reader finds reactions at all.
|
|
8319
|
+
- The quick row in the menu is MenuReactions (checkbox menu items) and the one in the pill's popover is ReactionPicker (a toolbar), so each is reachable by the keyboard model of the surface it sits in.
|
|
8320
|
+
- Only the reader's own reaction celebrates. Counts from other people roll quietly, and the reactions a page loads with never play.
|
|
8321
|
+
- The add-reaction pill appears only on messages that already have reactions; a message with none is reacted to from its menu, which keeps quiet rows quiet.
|
|
8322
|
+
|
|
8323
|
+
## Accessibility
|
|
8324
|
+
|
|
8325
|
+
- Each bubble is focusable, and its menu opens on right click, long press, Shift+F10 or the Menu key; its reactions are checkbox items reporting whether the reader has used each one.
|
|
8326
|
+
- Each reaction pill is a toggle named by its meaning and count ("Party, 3") with `aria-pressed`; the emoji and the burst are hidden from assistive tech.
|
|
8327
|
+
- Under reduced motion the reaction appears and the count changes at once; nothing else is lost.
|
|
8328
|
+
|
|
8329
|
+
## Source
|
|
8330
|
+
|
|
8331
|
+
```tsx
|
|
8332
|
+
import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Card, CardContent, CardFooter, cn, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, DEFAULT_REACTIONS, MenuReactions, Message } from "@sableui/react"
|
|
8333
|
+
|
|
8334
|
+
const messages: { id: string; text: string; mine: boolean; reactions: Record<string, number> }[] = [
|
|
8335
|
+
{ id: "m1", text: "The new onboarding flow is live on staging 🚀", mine: false, reactions: { "🎉": 2, "👀": 1 } },
|
|
8336
|
+
{ id: "m2", text: "Clicked through it twice. The progress bar is so smooth.", mine: true, reactions: {} },
|
|
8337
|
+
{ id: "m3", text: "Ship it Friday?", mine: false, reactions: { "👍": 1 } },
|
|
8338
|
+
]
|
|
8339
|
+
const hint = "Right-click or long-press a message to react."
|
|
8340
|
+
// Every emoji that can appear needs a name: it is the pill's accessible label.
|
|
8341
|
+
const labels: Record<string, string> = {
|
|
8342
|
+
...Object.fromEntries(DEFAULT_REACTIONS.map((r) => [r.emoji, r.label])),
|
|
8343
|
+
"👀": "Eyes",
|
|
8344
|
+
}
|
|
8345
|
+
|
|
8346
|
+
export interface MessageReactionsBlockProps {
|
|
8347
|
+
className?: string
|
|
8348
|
+
/** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
|
|
8349
|
+
framed?: boolean
|
|
8350
|
+
/** Called when the reader adds or removes a reaction, with the message, the emoji and whether it is now on. */
|
|
8351
|
+
onReact?: (messageId: string, emoji: string, on: boolean) => void
|
|
8352
|
+
}
|
|
8353
|
+
|
|
8354
|
+
export function MessageReactionsBlock({ className, framed = true, onReact }: MessageReactionsBlockProps) {
|
|
8355
|
+
// The reader's own reactions, per message. Everyone else's are the counts above.
|
|
8356
|
+
const [mine, setMine] = useState<Record<string, string[]>>({ m1: ["🎉"] })
|
|
8357
|
+
const toggle = (messageId: string, emoji: string) => {
|
|
8358
|
+
const on = !(mine[messageId] ?? []).includes(emoji)
|
|
8359
|
+
// Decided again from `prev`, so two toggles in one batch cannot add an emoji twice.
|
|
8360
|
+
setMine((prev) => {
|
|
8361
|
+
const current = prev[messageId] ?? []
|
|
8362
|
+
const next = current.includes(emoji) ? current.filter((e) => e !== emoji) : [...current, emoji]
|
|
8363
|
+
return { ...prev, [messageId]: next }
|
|
8364
|
+
})
|
|
8365
|
+
onReact?.(messageId, emoji, on)
|
|
8366
|
+
}
|
|
8367
|
+
|
|
8368
|
+
return (
|
|
8369
|
+
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-sm", className)}>
|
|
8370
|
+
<CardContent className="flex flex-col gap-3">
|
|
8371
|
+
{messages.map((m) => {
|
|
8372
|
+
const align = m.mine ? "end" : "start"
|
|
8373
|
+
const chosen = mine[m.id] ?? []
|
|
8374
|
+
// Others' counts, plus the reader's own: a reaction only the reader gave
|
|
8375
|
+
// is a pill of its own, ordered after the ones already there.
|
|
8376
|
+
const counts: Record<string, number> = { ...m.reactions }
|
|
8377
|
+
for (const emoji of chosen) counts[emoji] = (counts[emoji] ?? 0)
|
|
8378
|
+
const pills = Object.entries(counts).map(([emoji, others]) => ({
|
|
8379
|
+
emoji,
|
|
8380
|
+
active: chosen.includes(emoji),
|
|
8381
|
+
count: others + (chosen.includes(emoji) ? 1 : 0),
|
|
8382
|
+
}))
|
|
8383
|
+
return (
|
|
8384
|
+
<Message key={m.id} messageId={m.id} align={m.mine ? "end" : undefined}>
|
|
8385
|
+
<ContextMenu>
|
|
8386
|
+
<ContextMenuTrigger
|
|
8387
|
+
// Focusable, so Shift+F10 or the Menu key opens the same menu.
|
|
8388
|
+
render={<Bubble tabIndex={0} variant={m.mine ? "primary" : undefined} align={align} />}
|
|
8389
|
+
>
|
|
8390
|
+
{m.text}
|
|
8391
|
+
</ContextMenuTrigger>
|
|
8392
|
+
<ContextMenuContent>
|
|
8393
|
+
<MenuReactions selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />
|
|
8394
|
+
<ContextMenuSeparator />
|
|
8395
|
+
<ContextMenuItem>Reply</ContextMenuItem>
|
|
8396
|
+
<ContextMenuItem>Copy text</ContextMenuItem>
|
|
8397
|
+
</ContextMenuContent>
|
|
8398
|
+
</ContextMenu>
|
|
8399
|
+
{/* Rendered even while empty (it hides itself), so the first reaction still celebrates. */}
|
|
8400
|
+
<BubbleReactions align={align}>
|
|
8401
|
+
{pills.map((p) => (
|
|
8402
|
+
<BubbleReaction
|
|
8403
|
+
key={p.emoji}
|
|
8404
|
+
emoji={p.emoji}
|
|
8405
|
+
label={labels[p.emoji]}
|
|
8406
|
+
count={p.count}
|
|
8407
|
+
active={p.active}
|
|
8408
|
+
onClick={() => toggle(m.id, p.emoji)}
|
|
8409
|
+
/>
|
|
8410
|
+
))}
|
|
8411
|
+
{pills.length > 0 ? (
|
|
8412
|
+
<BubbleReactionAdd selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />
|
|
8413
|
+
) : null}
|
|
8414
|
+
</BubbleReactions>
|
|
8415
|
+
</Message>
|
|
8416
|
+
)
|
|
8417
|
+
})}
|
|
8418
|
+
</CardContent>
|
|
8419
|
+
<CardFooter className="text-caption text-muted-foreground">{hint}</CardFooter>
|
|
8420
|
+
</Card>
|
|
8421
|
+
)
|
|
8422
|
+
}
|
|
8423
|
+
```
|
|
8424
|
+
|
|
8425
|
+
---
|
|
8426
|
+
|
|
8102
8427
|
# NewsletterBlock
|
|
8103
8428
|
|
|
8104
8429
|
A newsletter sign-up card: the list's name, what it sends and how often, and an email field with Subscribe.
|
|
@@ -10655,6 +10980,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
|
|
|
10655
10980
|
- **arrival** · Root · _no motion_ — An accordion is page furniture that is present from first paint. Animating it in would stage content the reader is already scanning past.
|
|
10656
10981
|
- **response** · AccordionTrigger · `motion-interactive` — The whole header row is the hit target, and it looks like a heading rather than a control. The hover shift is what tells the reader it can be clicked before they click it.
|
|
10657
10982
|
- **transition** · AccordionPanel · `motion-disclosure` — Panel height is the only cue that content came from THIS header rather than appearing at the bottom of the page. Snapping it open moves everything below by a screenful with nothing connecting cause to effect.
|
|
10983
|
+
- **transition** · Chevron · `motion-interactive` — The chevron turning is the header's half of the same event as the panel opening. motion-interactive lists `rotate` (what Tailwind's rotate-* writes), so it turns with the panel instead of flipping 180° in one frame beside an easing panel. A custom `indicator` that rotates should carry motion-interactive itself.
|
|
10658
10984
|
- **activity** · Root · _no motion_ — Nothing here is indeterminate — the panel content is already resident. A panel that loads on expand should put its own loader inside the panel.
|
|
10659
10985
|
|
|
10660
10986
|
## Accessibility
|
|
@@ -10663,7 +10989,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
|
|
|
10663
10989
|
- Arrow keys move roving focus between triggers; Home/End jump to the first/last; loopFocus wraps by default.
|
|
10664
10990
|
- Each trigger toggles its panel with Enter/Space; the panel is announced as a labelled region.
|
|
10665
10991
|
- A disabled AccordionItem dims and is skipped by keyboard focus while neighbouring items stay interactive.
|
|
10666
|
-
- The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible
|
|
10992
|
+
- The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible; both ride motion-interactive, so reduced motion stills them with everything else.
|
|
10667
10993
|
|
|
10668
10994
|
## Related
|
|
10669
10995
|
|
|
@@ -10692,7 +11018,7 @@ import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@sab
|
|
|
10692
11018
|
```tsx
|
|
10693
11019
|
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, Icon } from "@sableui/react"
|
|
10694
11020
|
|
|
10695
|
-
<Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger indicator={<Icon name="plus" className="text-xs text-muted-foreground
|
|
11021
|
+
<Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger indicator={<Icon name="plus" className="text-xs text-muted-foreground motion-interactive [[data-panel-open]_&]:rotate-45" />}>Can I cancel anytime?</AccordionTrigger><AccordionPanel>Yes — from billing settings.</AccordionPanel></AccordionItem></Accordion>
|
|
10696
11022
|
```
|
|
10697
11023
|
|
|
10698
11024
|
---
|
|
@@ -10955,7 +11281,8 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
|
|
|
10955
11281
|
| grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
|
|
10956
11282
|
| xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
|
|
10957
11283
|
| yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
|
|
10958
|
-
|
|
|
11284
|
+
| 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 (draw-on, rising fill) and the data-change morph are the flourish; the gliding cursor and tooltip are the feedback `subtle` keeps. |
|
|
11285
|
+
| animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`. |
|
|
10959
11286
|
| activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
|
|
10960
11287
|
| defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
|
|
10961
11288
|
| onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
|
|
@@ -10969,9 +11296,9 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
|
|
|
10969
11296
|
|
|
10970
11297
|
## Motion
|
|
10971
11298
|
|
|
10972
|
-
- **arrival** · Area + line · _custom (outside the recipe layer)_ — Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. On the motion tokens; reduced motion shows the finished chart.
|
|
11299
|
+
- **arrival** · Area + line · _custom (outside the recipe layer)_ — Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. 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.
|
|
10973
11300
|
- **response** · Cursor + tooltip · _no motion_ — Hover and focus move the cursor and tooltip instantly — a lagging cursor reads as a slow chart.
|
|
10974
|
-
- **transition** · Area + line · _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.
|
|
11301
|
+
- **transition** · Area + line · _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.
|
|
10975
11302
|
- **activity** · AreaChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
|
|
10976
11303
|
|
|
10977
11304
|
## Accessibility
|
|
@@ -11090,6 +11417,13 @@ The card a file takes in a conversation — in the composer while it uploads, an
|
|
|
11090
11417
|
|
|
11091
11418
|
`card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`
|
|
11092
11419
|
|
|
11420
|
+
## Motion
|
|
11421
|
+
|
|
11422
|
+
- **arrival** · Root · _no motion_ — An attachment lands as part of a composer or a message, and that surface owns the entrance. A card that animated in on its own would read as a second, separate event — as if another file had been added.
|
|
11423
|
+
- **response** · Root · `motion-interactive` — With `onOpen`, the edge steps up under the pointer; it is the only cue that the card itself opens the file, not just its actions. Eased, it reads as the card answering the cursor rather than a border flickering as the pointer crosses a busy composer.
|
|
11424
|
+
- **transition** · Root · `motion-interactive` — Uploading to failed re-colours the edge and fill without changing the shape. The ease keeps it legible as the same file turning red; snapped, a failed card looks like a different card replaced the one that was sent.
|
|
11425
|
+
- **activity** · Progress · `motion-pulse` — While uploading with no known `progress`, the bar is indeterminate and pulses (Progress's own recipe); with a number, the fill eases toward each new value. A still bar at a fixed width is indistinguishable from an upload that has stalled.
|
|
11426
|
+
|
|
11093
11427
|
## Accessibility
|
|
11094
11428
|
|
|
11095
11429
|
- The full-card trigger is a real stretched `<button>` with an accessible label, not a click handler on a div — so it is one tab stop, keyboard-activatable, and focus-ringed.
|
|
@@ -11606,7 +11940,8 @@ Use BarChart to compare amounts across categories — the default chart for "how
|
|
|
11606
11940
|
| grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
|
|
11607
11941
|
| xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
|
|
11608
11942
|
| yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
|
|
11609
|
-
|
|
|
11943
|
+
| 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 (bars rising in sequence) and the data-change morph are the flourish; the hover dim and gliding cursor are the feedback `subtle` keeps. |
|
|
11944
|
+
| animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`. |
|
|
11610
11945
|
| activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
|
|
11611
11946
|
| defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
|
|
11612
11947
|
| onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
|
|
@@ -11624,9 +11959,9 @@ Use BarChart to compare amounts across categories — the default chart for "how
|
|
|
11624
11959
|
|
|
11625
11960
|
## Motion
|
|
11626
11961
|
|
|
11627
|
-
- **arrival** · Bar · _custom (outside the recipe layer)_ — Once on screen, bars rise from the baseline in sequence on the emphasized easing (scale from the zero line) and their labels fade in. On the motion tokens; reduced motion shows the finished chart.
|
|
11628
|
-
- **response** · Cursor + tooltip · _no motion_ — The hovered category gets a band behind it and the other bars dim at once — a lagging highlight reads as a slow chart.
|
|
11629
|
-
- **transition** · Bar · _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.
|
|
11962
|
+
- **arrival** · Bar · _custom (outside the recipe layer)_ — Once on screen, bars rise from the baseline in sequence on the emphasized easing (scale from the zero line) and their 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.
|
|
11963
|
+
- **response** · Cursor + tooltip · _no motion_ — The hovered category gets a band behind it and the other bars dim at once — a lagging highlight reads as a slow chart. The dim mixes each bar toward the surface instead of fading it, so gridlines never strike through a dimmed column and stacked segments stay solid. Essential: `subtle` keeps it.
|
|
11964
|
+
- **transition** · Bar · _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.
|
|
11630
11965
|
- **activity** · BarChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
|
|
11631
11966
|
|
|
11632
11967
|
## Accessibility
|
|
@@ -11740,6 +12075,14 @@ Use BottomNav for a small set of top-level mobile destinations that should remai
|
|
|
11740
12075
|
|
|
11741
12076
|
`background`, `border`, `muted-foreground`, `foreground`, `ring`
|
|
11742
12077
|
|
|
12078
|
+
## Motion
|
|
12079
|
+
|
|
12080
|
+
- **arrival** · Root · _no motion_ — Fixed app chrome, present from the first paint of every screen. Animating it in on navigation would make each route change look like the app restarting.
|
|
12081
|
+
- **response** · BottomNavItem · `motion-interactive` — The ink steps up on hover and the focus ring eases in, so a pointer or a keyboard on a tablet gets the same answer as every other control in the app.
|
|
12082
|
+
- **response** · BottomNavItem · `motion-press` — On a phone there is no hover to say a tap landed, and a route can take a beat to paint. The settle on press is the receipt; without it people tap the tab a second time.
|
|
12083
|
+
- **transition** · BottomNavItem · `motion-interactive` — Changing tab eases the active ink out of one item and into the next. It is a cross-fade, not a travelling indicator: each item owns its colour, so there is no single mark to glide.
|
|
12084
|
+
- **activity** · Root · _no motion_ — Navigation has no working state of its own. A slow route shows its pending state in the page it is loading, not in the tab bar.
|
|
12085
|
+
|
|
11743
12086
|
## Accessibility
|
|
11744
12087
|
|
|
11745
12088
|
- BottomNav renders a <nav> landmark.
|
|
@@ -11834,6 +12177,13 @@ Use Breadcrumbs to show the user's location inside a hierarchy and provide light
|
|
|
11834
12177
|
|
|
11835
12178
|
`muted-foreground`, `foreground`
|
|
11836
12179
|
|
|
12180
|
+
## Motion
|
|
12181
|
+
|
|
12182
|
+
- **arrival** · Root · _no motion_ — The trail is page furniture, rendered with the page header. An entrance would replay on every navigation, which is exactly when the reader looks at it to get their bearings.
|
|
12183
|
+
- **response** · BreadcrumbLink · `motion-interactive` — Ancestors are muted until hovered or focused. Easing the ink makes the trail read as links answering the pointer; a hard snap across a row of small text looks like flicker.
|
|
12184
|
+
- **transition** · Root · _no motion_ — A breadcrumb has no state of its own. The current page changes by navigation, which replaces the trail; animating between two trails would imply the path itself moved.
|
|
12185
|
+
- **activity** · Root · _no motion_ — It never works in the background. A route that is still loading says so in the page, not in its address.
|
|
12186
|
+
|
|
11837
12187
|
## Accessibility
|
|
11838
12188
|
|
|
11839
12189
|
- The <nav> carries aria-label="Breadcrumb" so assistive tech announces the landmark.
|
|
@@ -11965,14 +12315,16 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
|
|
|
11965
12315
|
| --- | --- | --- |
|
|
11966
12316
|
| Bubble | yes | The surface itself. Holds the variant, the side, and the tail. |
|
|
11967
12317
|
| BubbleReactions | no | The row of reaction pills, just under the bubble at the message column's small gap, so it reads as belonging to that message rather than the next row. |
|
|
11968
|
-
| BubbleReaction | no | One reaction pill — emoji, count (hidden at 1), and pressed state. |
|
|
12318
|
+
| BubbleReaction | no | One reaction pill — emoji, count (hidden at 1), and pressed state. Celebrates when it becomes the reader's own. |
|
|
12319
|
+
| BubbleReactionAdd | no | The add-reaction pill at the end of the row: opens a ReactionPicker above it. |
|
|
11969
12320
|
|
|
11970
12321
|
## Subcomponents
|
|
11971
12322
|
|
|
11972
12323
|
| Name | Type | Description |
|
|
11973
12324
|
| --- | --- | --- |
|
|
11974
12325
|
| BubbleReactions | { align?: "start" \| "end" } & ComponentPropsWithRef<"div"> | Row container for reaction pills. Match `align` to the Bubble so the row hangs off the same edge. |
|
|
11975
|
-
| BubbleReaction | { emoji: ReactNode; count?: number; active?: boolean } & ComponentPropsWithRef<"button"> | One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, and `count` is hidden at 1 because the pill already says "one". |
|
|
12326
|
+
| BubbleReaction | { emoji: ReactNode; label?: string; count?: number; active?: boolean; motion?: MotionLevel } & ComponentPropsWithRef<"button"> | One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, `label` names it ("Party, 3"), and `count` is hidden at 1 because the pill already says "one". When it turns on — pressed, or newly added already on — the emoji blooms, copies of it burst out and the count rolls; pills the page loads with stay still. |
|
|
12327
|
+
| BubbleReactionAdd | { reactions?: Reaction[]; selected?: string[]; onSelect?: (emoji: string) => void; label?: string; motion?: MotionLevel } & ComponentPropsWithRef<"button"> | The add-reaction pill: a smiley that opens a ReactionPicker above it. Choosing an emoji closes the picker and calls `onSelect`; add the emoji to the row and its new pill celebrates on arrival. |
|
|
11976
12328
|
|
|
11977
12329
|
## Props
|
|
11978
12330
|
|
|
@@ -11993,16 +12345,23 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
|
|
|
11993
12345
|
|
|
11994
12346
|
`muted`, `foreground`, `primary`, `primary-foreground`, `border`, `card`, `card-foreground`, `accent`, `accent-foreground`, `muted-foreground`, `ring`
|
|
11995
12347
|
|
|
12348
|
+
## Motion
|
|
12349
|
+
|
|
12350
|
+
- **arrival** · Bubble · _no motion_ — A bubble arrives with its Message row, which owns how a new message enters the transcript. A reaction pill that the reader just added arrives with its own celebration (below).
|
|
12351
|
+
- **response** · BubbleReaction · `motion-bloom` — Reacting is the reader's choice, so it is answered like a like: the emoji grows from nothing past its size and settles, while copies of it spray outward and fade (Burst with the emoji as its particle, the way chat apps answer a reaction). Plays when a pill turns on and when a pill mounts already on into a row that has rendered — never for the pills a page loads with. Pills inherit Pill's motion-interactive and motion-press.
|
|
12352
|
+
- **transition** · BubbleReaction · _custom (outside the recipe layer)_ — The count rolls with NumberTicker as people react. Turning a reaction off is quiet: the tint eases out and the count rolls back, with no burst. Under `subtle` or reduced motion the bloom and the spray are dropped and the tint and count alone answer.
|
|
12353
|
+
- **activity** · Bubble · _no motion_ — A bubble is static content; a message still being written is StreamingText or a Marker, not a state of the surface.
|
|
12354
|
+
|
|
11996
12355
|
## Accessibility
|
|
11997
12356
|
|
|
11998
12357
|
- The surface is a plain container with no imposed role — a bubble is presentation, and announcing one as a region would put a landmark around every line of a conversation.
|
|
11999
|
-
- Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone
|
|
12358
|
+
- Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone. Give each a `label`: it is named "Party, 3", with the emoji and its burst hidden from AT. The add-reaction pill is named "Add reaction" and opens a toolbar picker.
|
|
12000
12359
|
- `whitespace-pre-wrap` and `break-words` keep pasted logs, long URLs, and hard-wrapped text inside the surface instead of overflowing the conversation.
|
|
12001
12360
|
- Every variant meets AA against its own surface, including `primary`, which pairs the primary fill with its paired foreground token rather than a hardcoded white.
|
|
12002
12361
|
|
|
12003
12362
|
## Related
|
|
12004
12363
|
|
|
12005
|
-
- Components: Message, MessageScroller, Marker, Attachment
|
|
12364
|
+
- Components: Message, MessageScroller, Marker, Attachment, ReactionPicker
|
|
12006
12365
|
|
|
12007
12366
|
## Examples
|
|
12008
12367
|
|
|
@@ -12033,13 +12392,14 @@ import { Avatar, Bubble, CodeBlock, Message } from "@sableui/react"
|
|
|
12033
12392
|
### With reactions
|
|
12034
12393
|
|
|
12035
12394
|
```tsx
|
|
12036
|
-
import { Bubble, BubbleReaction, BubbleReactions, Message } from "@sableui/react"
|
|
12395
|
+
import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Message } from "@sableui/react"
|
|
12037
12396
|
|
|
12038
12397
|
<Message messageId="m3">
|
|
12039
12398
|
<Bubble>Shipped 0.45.0.</Bubble>
|
|
12040
12399
|
<BubbleReactions>
|
|
12041
|
-
<BubbleReaction emoji="🎉" count={3} active onClick={() => toggle("🎉")} />
|
|
12042
|
-
<BubbleReaction emoji="👀" count={1} onClick={() => toggle("👀")} />
|
|
12400
|
+
<BubbleReaction emoji="🎉" label="Party" count={3} active onClick={() => toggle("🎉")} />
|
|
12401
|
+
<BubbleReaction emoji="👀" label="Eyes" count={1} onClick={() => toggle("👀")} />
|
|
12402
|
+
<BubbleReactionAdd selected={mine} onSelect={toggle} />
|
|
12043
12403
|
</BubbleReactions>
|
|
12044
12404
|
</Message>
|
|
12045
12405
|
```
|
|
@@ -12072,7 +12432,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
12072
12432
|
### Best practices
|
|
12073
12433
|
|
|
12074
12434
|
- Do: Use a single primary button in a view or dense panel so the action hierarchy stays obvious.
|
|
12075
|
-
- Do: For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`. Text that costs something to produce (a file to fetch) goes in as a function, `useCopy(() => fetch(url).then((r) => r.text()))`, called only on click.
|
|
12435
|
+
- Do: For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`. Text that costs something to produce (a file to fetch) goes in as a function, `useCopy(() => fetch(url).then((r) => r.text()))`, called only on click. Keep both glyphs mounted in a `motion-icon-swap` cell so the copy mark visibly becomes a check (a `copied ? "check" : "copy"` swap snaps between frames and is easy to miss), and say "Copied" in a polite `role="status"` region: the glyph change is silent to a screen reader.
|
|
12076
12436
|
- Do: Write labels as actions, such as "Save changes" or "Delete draft", instead of vague labels like "OK".
|
|
12077
12437
|
- Do: Use loading with loadingLabel for async work so the action cannot double-fire and the state is announced.
|
|
12078
12438
|
- Do: Put icons in iconStart / iconEnd rather than as children, so the button applies its optical padding. Start icons say what the action is (plus, share); end icons say where it goes (chevron-right, external).
|
|
@@ -12100,10 +12460,10 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
12100
12460
|
| variant | "primary" \| "secondary" \| "ghost" \| "outline" \| "destructive" \| "link" \| "floating" \| "field" \| "on-color" \| "on-color-outline" \| "on-color-ghost" | "primary" | Visual emphasis tier. One primary per screen. The on-color tiers are for buttons ON a coloured fill: they paint with the inherited text colour (the fill's ink) — on-color is solid (ink background, fill-coloured label via the optional --on-color-fill var, else black/white), on-color-outline and on-color-ghost are transparent with an ink label. Never use them on page or card surfaces. floating is for a control that sits OVER content (carousel arrows, a marquee pause, a jump-to-latest over a conversation): its own edge, lift and translucent popover fill, so it reads on arbitrary content beneath. field is for a button docked in a workspace panel among xs fields: the inset panel-field fill and the field's corner, so a picker trigger or an add action reads as one of the row's controls; pair it with size xs or icon-xs. |
|
|
12101
12461
|
| size | "xs" \| "sm" \| "md" \| "cta" \| "icon" \| "icon-sm" \| "icon-xs" | "md" | Reads the density ladder, so every value differs by register: page 28/32/40/44, workspace 24/28/32/36. xs and icon-xs were added in 0.31.0 for the workspace control floor and are the sizes tool chrome wants; md is the default everywhere. icon/icon-sm/icon-xs are square. |
|
|
12102
12462
|
| render | ReactElement \| (props, state) => ReactElement | — | Compose with another element/component through the Base UI render prop. |
|
|
12103
|
-
| loading | boolean | false | Show a
|
|
12463
|
+
| loading | boolean | false | Show a Spinner while an action is in flight and refuse presses. The spinner takes the iconStart slot when there is one; otherwise it lays over the label, which fades out in place, so the button keeps its width. Sets aria-busy and aria-disabled, never the native disabled: disabling the focused button sent focus to <body>, so a sheet the action opened had nowhere to return it. |
|
|
12104
12464
|
| onClick | (event: MouseEvent<HTMLButtonElement>) => unknown | — | May return a promise: the button shows `loading` until it settles and ignores further presses meanwhile, from the first click on. Prefer it to a `busy` flag in state, which takes effect a render late (three fast clicks ran three times). |
|
|
12105
12465
|
| badge | ReactNode | — | A corner indicator (usually <NotificationBadge dot /> or a count) overlaid on the top-end corner of the glyph. For icon-only sizes; never hand-position a badge over a Button with absolute offsets. |
|
|
12106
|
-
| loadingLabel | ReactNode | — | Optional text shown in place of children while loading. |
|
|
12466
|
+
| loadingLabel | ReactNode | — | Optional text shown in place of children while loading, with the spinner beside it. The width follows the new text, so prefer no loadingLabel where the row must not move. |
|
|
12107
12467
|
| iconStart | ReactNode | — | Icon before the label. The button takes one 2px step off its start padding so icon + label read as centred. Replaced by the spinner while loading. |
|
|
12108
12468
|
| iconEnd | ReactNode | — | Icon after the label (disclosure, external link, next). Same optical correction on the end side. |
|
|
12109
12469
|
| bleed | "start" \| "end" \| "both" \| "none" | — | Optical alignment for a borderless button (ghost, link, on-color-ghost): a negative inline margin of exactly its own padding on that side — --button-px, or the icon side's optical padding when an icon sits there; an icon-only size bleeds by its inset to the glyph — so the label (or glyph) lands on the content edge. Hit area and hover/focus fill keep their full size. Logical sides, so it mirrors in RTL. A no-op on variants with a visible edge (primary, secondary, outline, destructive, floating, field, on-color, on-color-outline): a filled button aligns its edge, not its label. Unset, a borderless button renders data-bleed="auto" and SectionHeader, PageHeader and CardAction bleed it when it is their outermost child; none opts out. |
|
|
@@ -12122,7 +12482,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
12122
12482
|
- **arrival** · Root · _no motion_ — A button belongs to the layout it ships in. Whatever staged that region owns the entrance; a button that fades in on its own is a button that is briefly un-clickable for no stated reason.
|
|
12123
12483
|
- **response** · Root · `motion-interactive` — Hover, focus ring and disabled fade all ease on one declaration. This is the reference case for the whole recipe: it lists scale and translate alongside the colours specifically so motion-press can add a transform without a second, conflicting transition-property.
|
|
12124
12484
|
- **response** · Root · `motion-press` — The 0.97 settle on pointer-down is the only confirmation the press registered before the action's own result arrives — which on a slow network is the difference between waiting and clicking again. It skips disabled elements, so a dead button stays visibly dead.
|
|
12125
|
-
- **transition** ·
|
|
12485
|
+
- **transition** · Spinner · `motion-appear` — Going busy is the one state change a button has. The spinner fades in where it mounts — in the icon slot, or laid over a plain label that fades out beneath it and keeps its place — so the width never changes and the row does not shift under the pointer that just pressed it. The label fades back when the work settles. A toggling control is a Switch or a Toggle; those carry their own transition.
|
|
12126
12486
|
- **activity** · Root · _no motion_ — Pending work is passed in via `loading`, which renders a Spinner — the activity motion lives there rather than being duplicated here.
|
|
12127
12487
|
|
|
12128
12488
|
## Accessibility
|
|
@@ -12177,10 +12537,10 @@ import { Button, Icon, Menu, MenuContent, MenuItem, MenuTrigger, Row } from "@sa
|
|
|
12177
12537
|
### Copy to clipboard
|
|
12178
12538
|
|
|
12179
12539
|
```tsx
|
|
12180
|
-
import { Button, Icon, useCopy } from "@sableui/react"
|
|
12540
|
+
import { Button, Icon, Row, useCopy } from "@sableui/react"
|
|
12181
12541
|
|
|
12182
12542
|
const { copy, copied } = useCopy(address)
|
|
12183
|
-
<Button variant="ghost" size="icon-sm" aria-label="Copy address" onClick={copy}><Icon name={copied
|
|
12543
|
+
<Row gap={1}><Button variant="ghost" size="icon-sm" aria-label="Copy address" 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>
|
|
12184
12544
|
```
|
|
12185
12545
|
|
|
12186
12546
|
### As link
|
|
@@ -12382,6 +12742,7 @@ Use Calendar to browse or choose dates in a month grid, especially when relative
|
|
|
12382
12742
|
| disabled | Matcher \| Matcher[] | — | Days matching this are dimmed and non-selectable (e.g. { before: new Date() }). |
|
|
12383
12743
|
| showOutsideDays | boolean | true | Render leading/trailing days from adjacent months, dimmed. |
|
|
12384
12744
|
| 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. |
|
|
12745
|
+
| 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. |
|
|
12385
12746
|
| className | string | — | Merged onto the bordered card container that wraps the grid. |
|
|
12386
12747
|
| 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. |
|
|
12387
12748
|
| components | Partial<CustomComponents> | — | Render overrides for RDP parts. Chevron is supplied by default and merged with yours. |
|
|
@@ -12394,6 +12755,14 @@ Use Calendar to browse or choose dates in a month grid, especially when relative
|
|
|
12394
12755
|
|
|
12395
12756
|
`card`, `card-foreground`, `border`, `primary`, `primary-foreground`, `accent`, `accent-foreground`, `muted-foreground`, `ring`
|
|
12396
12757
|
|
|
12758
|
+
## Motion
|
|
12759
|
+
|
|
12760
|
+
- **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.
|
|
12761
|
+
- **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.
|
|
12762
|
+
- **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.
|
|
12763
|
+
- **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.
|
|
12764
|
+
- **activity** · Root · _no motion_ — Days are resident data; nothing in the grid loads. Availability fetched per month belongs to the surface around the calendar.
|
|
12765
|
+
|
|
12397
12766
|
## Accessibility
|
|
12398
12767
|
|
|
12399
12768
|
- react-day-picker manages the roving-tabindex grid, arrow-key navigation, and ARIA roles/labels for the calendar.
|
|
@@ -12707,11 +13076,20 @@ Use Carousel for a curated, finite sequence of media or cards that benefits from
|
|
|
12707
13076
|
| label | string | — | Accessible label for the carousel region. |
|
|
12708
13077
|
| slideClassName | string | — | Class for each slide wrapper (e.g. an aspect ratio). |
|
|
12709
13078
|
| 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. |
|
|
13079
|
+
| 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. |
|
|
12710
13080
|
|
|
12711
13081
|
## Tokens used
|
|
12712
13082
|
|
|
12713
13083
|
`border`, `ring`, `popover`, `popover-foreground`, `foreground`, `muted-foreground`
|
|
12714
13084
|
|
|
13085
|
+
## Motion
|
|
13086
|
+
|
|
13087
|
+
- **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.
|
|
13088
|
+
- **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.
|
|
13089
|
+
- **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.
|
|
13090
|
+
- **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.
|
|
13091
|
+
- **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.
|
|
13092
|
+
|
|
12715
13093
|
## Accessibility
|
|
12716
13094
|
|
|
12717
13095
|
- Region carries aria-roledescription='carousel'; each slide is a group with aria-roledescription='slide' and an 'N of M' label; clones are aria-hidden.
|
|
@@ -12757,6 +13135,14 @@ import { Carousel } from "@sableui/react"
|
|
|
12757
13135
|
<Carousel loop autoplay={4000} counter>{slides}</Carousel>
|
|
12758
13136
|
```
|
|
12759
13137
|
|
|
13138
|
+
### Subtle motion
|
|
13139
|
+
|
|
13140
|
+
```tsx
|
|
13141
|
+
import { Carousel } from "@sableui/react"
|
|
13142
|
+
|
|
13143
|
+
<Carousel loop motion="subtle" label="Gallery">{slides}</Carousel>
|
|
13144
|
+
```
|
|
13145
|
+
|
|
12760
13146
|
---
|
|
12761
13147
|
|
|
12762
13148
|
# ChatComposer
|
|
@@ -12819,6 +13205,7 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
|
|
|
12819
13205
|
| stopLabel | string | "Stop generating" | Accessible label for the stop button. |
|
|
12820
13206
|
| 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}`. |
|
|
12821
13207
|
| 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. |
|
|
13208
|
+
| 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. |
|
|
12822
13209
|
| ...form props | Omit<ComponentPropsWithRef<"form">, "onSubmit" \| "defaultValue"> | — | Spread onto the form element. Use `onSend` rather than `onSubmit`. |
|
|
12823
13210
|
|
|
12824
13211
|
## Variants
|
|
@@ -12829,6 +13216,14 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
|
|
|
12829
13216
|
|
|
12830
13217
|
`card`, `input`, `ring`, `destructive`, `foreground`, `muted-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`
|
|
12831
13218
|
|
|
13219
|
+
## Motion
|
|
13220
|
+
|
|
13221
|
+
- **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.
|
|
13222
|
+
- **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.
|
|
13223
|
+
- **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.
|
|
13224
|
+
- **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.
|
|
13225
|
+
- **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.
|
|
13226
|
+
|
|
12832
13227
|
## Accessibility
|
|
12833
13228
|
|
|
12834
13229
|
- 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.
|
|
@@ -12939,11 +13334,19 @@ Use Checkbox for independent boolean choices or for selecting multiple options i
|
|
|
12939
13334
|
| name | string | — | Field name for the hidden input on form submit. |
|
|
12940
13335
|
| 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. |
|
|
12941
13336
|
| render | ReactElement \| (props, state) => ReactElement | — | Compose the root with another element through the Base UI render prop. |
|
|
13337
|
+
| 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. |
|
|
12942
13338
|
|
|
12943
13339
|
## Tokens used
|
|
12944
13340
|
|
|
12945
13341
|
`card`, `input`, `primary`, `primary-foreground`, `ring`
|
|
12946
13342
|
|
|
13343
|
+
## Motion
|
|
13344
|
+
|
|
13345
|
+
- **arrival** · Root · _no motion_ — It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence.
|
|
13346
|
+
- **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.
|
|
13347
|
+
- **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.
|
|
13348
|
+
- **activity** · Root · _no motion_ — A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control.
|
|
13349
|
+
|
|
12947
13350
|
## Accessibility
|
|
12948
13351
|
|
|
12949
13352
|
- Base UI renders a hidden native <input type=checkbox> for keyboard, focus, and form semantics.
|
|
@@ -13044,6 +13447,7 @@ Use CheckboxCard when an option needs more visual weight, supporting text, or ic
|
|
|
13044
13447
|
| tone | "neutral" \| "3" \| "2" \| "1" \| "info" \| "warning" | "neutral" | Semantic colour the checked state carries across the border, checkbox box, and icon. |
|
|
13045
13448
|
| 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. |
|
|
13046
13449
|
| 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. |
|
|
13450
|
+
| 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. |
|
|
13047
13451
|
|
|
13048
13452
|
## Variants
|
|
13049
13453
|
|
|
@@ -13053,6 +13457,13 @@ Use CheckboxCard when an option needs more visual weight, supporting text, or ic
|
|
|
13053
13457
|
|
|
13054
13458
|
`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`
|
|
13055
13459
|
|
|
13460
|
+
## Motion
|
|
13461
|
+
|
|
13462
|
+
- **arrival** · Root · _no motion_ — It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence.
|
|
13463
|
+
- **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.
|
|
13464
|
+
- **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.
|
|
13465
|
+
- **activity** · Root · _no motion_ — A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control.
|
|
13466
|
+
|
|
13056
13467
|
## Accessibility
|
|
13057
13468
|
|
|
13058
13469
|
- Base UI provides the checkbox role, aria-checked, hidden native input, keyboard toggle, and data-[checked]/data-disabled state.
|
|
@@ -13212,10 +13623,18 @@ Use CodeBlock for multiline code samples that users may read, inspect, or copy.
|
|
|
13212
13623
|
|
|
13213
13624
|
`border`, `muted`, `muted-foreground`, `foreground`
|
|
13214
13625
|
|
|
13626
|
+
## Motion
|
|
13627
|
+
|
|
13628
|
+
- **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.
|
|
13629
|
+
- **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.
|
|
13630
|
+
- **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.
|
|
13631
|
+
- **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.
|
|
13632
|
+
|
|
13215
13633
|
## Accessibility
|
|
13216
13634
|
|
|
13217
13635
|
- Code renders inside a <pre><code> for correct semantics and screen-reader handling.
|
|
13218
13636
|
- Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety.
|
|
13637
|
+
- 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.
|
|
13219
13638
|
|
|
13220
13639
|
## Related
|
|
13221
13640
|
|
|
@@ -13296,6 +13715,14 @@ Use Collapsible for a single piece of optional content that can expand in place.
|
|
|
13296
13715
|
|
|
13297
13716
|
`foreground`, `muted-foreground`
|
|
13298
13717
|
|
|
13718
|
+
## Motion
|
|
13719
|
+
|
|
13720
|
+
- **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.
|
|
13721
|
+
- **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.
|
|
13722
|
+
- **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.
|
|
13723
|
+
- **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.
|
|
13724
|
+
- **activity** · Root · _no motion_ — Nothing here is indeterminate; content that loads on open should put its own loader inside the panel.
|
|
13725
|
+
|
|
13299
13726
|
## Accessibility
|
|
13300
13727
|
|
|
13301
13728
|
- Base UI wires aria-expanded on the trigger and aria-controls/id between trigger and panel.
|
|
@@ -13379,6 +13806,13 @@ Use ColorField wherever a colour is a property: a canvas background, an effect p
|
|
|
13379
13806
|
|
|
13380
13807
|
`panel`, `panel-raised`, `hairline`, `hairline-strong`, `ring`, `popover`
|
|
13381
13808
|
|
|
13809
|
+
## Motion
|
|
13810
|
+
|
|
13811
|
+
- **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.
|
|
13812
|
+
- **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.
|
|
13813
|
+
- **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.
|
|
13814
|
+
- **activity** · Root · _no motion_ — Picking is synchronous. The EyeDropper is the browser's own modal, with its own feedback.
|
|
13815
|
+
|
|
13382
13816
|
## Accessibility
|
|
13383
13817
|
|
|
13384
13818
|
- 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.
|
|
@@ -13504,6 +13938,16 @@ Use Combobox when users choose from a list that benefits from typing, filtering,
|
|
|
13504
13938
|
|
|
13505
13939
|
`input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`, `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `primary`, `secondary`, `secondary-foreground`
|
|
13506
13940
|
|
|
13941
|
+
## Motion
|
|
13942
|
+
|
|
13943
|
+
- **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.
|
|
13944
|
+
- **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.
|
|
13945
|
+
- **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.
|
|
13946
|
+
- **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.
|
|
13947
|
+
- **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.
|
|
13948
|
+
- **transition** · ComboboxContent · `motion-pop` — Closing shrinks back toward the field, returning the eye to the input that now holds the value.
|
|
13949
|
+
- **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.
|
|
13950
|
+
|
|
13507
13951
|
## Accessibility
|
|
13508
13952
|
|
|
13509
13953
|
- Base UI provides role=combobox/listbox/option, roving tabindex, arrow-key navigation, and type-to-filter.
|
|
@@ -13644,6 +14088,15 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
|
|
|
13644
14088
|
|
|
13645
14089
|
`card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
|
|
13646
14090
|
|
|
14091
|
+
## Motion
|
|
14092
|
+
|
|
14093
|
+
- **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.
|
|
14094
|
+
- **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.
|
|
14095
|
+
- **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.
|
|
14096
|
+
- **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.
|
|
14097
|
+
- **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.
|
|
14098
|
+
- **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.
|
|
14099
|
+
|
|
13647
14100
|
## Accessibility
|
|
13648
14101
|
|
|
13649
14102
|
- 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.
|
|
@@ -13733,6 +14186,101 @@ useCommandMenuShortcut(() => setOpen((value) => !value))
|
|
|
13733
14186
|
|
|
13734
14187
|
---
|
|
13735
14188
|
|
|
14189
|
+
# ConfirmButton
|
|
14190
|
+
|
|
14191
|
+
**Status:** stable
|
|
14192
|
+
|
|
14193
|
+
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.
|
|
14194
|
+
|
|
14195
|
+
**Keywords:** confirm, confirmation, two-step, press again, click again, arm, delete, remove, destructive, inline confirm, hold to confirm
|
|
14196
|
+
|
|
14197
|
+
## Usage
|
|
14198
|
+
|
|
14199
|
+
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.
|
|
14200
|
+
|
|
14201
|
+
### Best practices
|
|
14202
|
+
|
|
14203
|
+
- 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.
|
|
14204
|
+
- 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.
|
|
14205
|
+
- 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.
|
|
14206
|
+
- 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**
|
|
14207
|
+
- 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**
|
|
14208
|
+
- 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**
|
|
14209
|
+
|
|
14210
|
+
## Props
|
|
14211
|
+
|
|
14212
|
+
| Name | Type | Default | Description |
|
|
14213
|
+
| --- | --- | --- | --- |
|
|
14214
|
+
| children | ReactNode | — | The idle label — the action, e.g. "Delete". |
|
|
14215
|
+
| 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. |
|
|
14216
|
+
| onConfirm | () => unknown | — | Runs on the second press while armed. Return a promise to hold the button armed with a spinner until it settles. |
|
|
14217
|
+
| timeout | number | 3000 | How long it waits for the second press, in ms. A decision time, so it is not scaled by motion settings. |
|
|
14218
|
+
| variant | ButtonVariant | "secondary" | The idle look — any Button variant. |
|
|
14219
|
+
| confirmVariant | ButtonVariant | "destructive" | The armed look — any Button variant. |
|
|
14220
|
+
| size | "xs" \| "sm" \| "md" \| "cta" | "md" | A labelled Button size. Icon-only sizes are excluded: the label is the interaction. |
|
|
14221
|
+
| icon | ReactNode | — | A leading icon kept through both states, e.g. `<Icon name="trash" />`. |
|
|
14222
|
+
| disabled | boolean | false | Disables it; disabling while armed also disarms. |
|
|
14223
|
+
| confirmHint | string | "Press again to confirm." | Read to assistive tech after the armed label when it arms. Override to localise. |
|
|
14224
|
+
| 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. |
|
|
14225
|
+
|
|
14226
|
+
## Variants
|
|
14227
|
+
|
|
14228
|
+
- **variant**: primary, secondary, ghost, outline, destructive, link, floating, field, on-color, on-color-outline, on-color-ghost
|
|
14229
|
+
- **confirmVariant**: destructive, primary, secondary, outline
|
|
14230
|
+
- **size**: xs, sm, md, cta
|
|
14231
|
+
|
|
14232
|
+
## Tokens used
|
|
14233
|
+
|
|
14234
|
+
`secondary`, `secondary-foreground`, `destructive`, `destructive-foreground`, `ring`
|
|
14235
|
+
|
|
14236
|
+
## Motion
|
|
14237
|
+
|
|
14238
|
+
- **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.
|
|
14239
|
+
- **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.
|
|
14240
|
+
- **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.
|
|
14241
|
+
- **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.
|
|
14242
|
+
|
|
14243
|
+
## Accessibility
|
|
14244
|
+
|
|
14245
|
+
- A native <button> through Button. Focus stays on it through both presses; Enter and Space confirm exactly as a click does.
|
|
14246
|
+
- Only the current label is in the accessible name — the other is aria-hidden — so the name changes to the question while armed.
|
|
14247
|
+
- 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.
|
|
14248
|
+
- 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.
|
|
14249
|
+
- A second press within 300ms of arming is ignored: a double-click is a habit, not a decision.
|
|
14250
|
+
- Under reduced motion the label still changes and the colour still turns; only the width easing and countdown are dropped.
|
|
14251
|
+
|
|
14252
|
+
## Related
|
|
14253
|
+
|
|
14254
|
+
- Components: Button, DestructiveActionModal, Toast, OverflowMenu
|
|
14255
|
+
|
|
14256
|
+
## Examples
|
|
14257
|
+
|
|
14258
|
+
### Delete a row
|
|
14259
|
+
|
|
14260
|
+
```tsx
|
|
14261
|
+
import { ConfirmButton, Icon } from "@sableui/react"
|
|
14262
|
+
|
|
14263
|
+
<ConfirmButton icon={<Icon name="trash" />} onConfirm={() => removeRow(id)}>Delete</ConfirmButton>
|
|
14264
|
+
```
|
|
14265
|
+
|
|
14266
|
+
### Custom question, longer wait
|
|
14267
|
+
|
|
14268
|
+
```tsx
|
|
14269
|
+
import { ConfirmButton } from "@sableui/react"
|
|
14270
|
+
|
|
14271
|
+
<ConfirmButton variant="ghost" size="sm" confirmLabel="Revoke this key?" timeout={5000} onConfirm={revokeKey}>Revoke</ConfirmButton>
|
|
14272
|
+
```
|
|
14273
|
+
|
|
14274
|
+
### Async confirm
|
|
14275
|
+
|
|
14276
|
+
```tsx
|
|
14277
|
+
import { ConfirmButton } from "@sableui/react"
|
|
14278
|
+
|
|
14279
|
+
<ConfirmButton onConfirm={() => api.deleteDraft(draft.id)}>Discard draft</ConfirmButton>
|
|
14280
|
+
```
|
|
14281
|
+
|
|
14282
|
+
---
|
|
14283
|
+
|
|
13736
14284
|
# ContextCard
|
|
13737
14285
|
|
|
13738
14286
|
**Status:** stable
|
|
@@ -13943,6 +14491,14 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
13943
14491
|
|
|
13944
14492
|
`muted-foreground`, `foreground`, `ring`
|
|
13945
14493
|
|
|
14494
|
+
## Motion
|
|
14495
|
+
|
|
14496
|
+
- **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.
|
|
14497
|
+
- **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.
|
|
14498
|
+
- **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.
|
|
14499
|
+
- **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.
|
|
14500
|
+
- **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.
|
|
14501
|
+
|
|
13946
14502
|
## Accessibility
|
|
13947
14503
|
|
|
13948
14504
|
- Sorted columns set aria-sort=ascending/descending on the header cell.
|
|
@@ -14746,6 +15302,13 @@ Use Entity to show a compact identity block for a person, organization, file, pr
|
|
|
14746
15302
|
|
|
14747
15303
|
`accent`, `accent-foreground`, `foreground`, `muted-foreground`, `ring`
|
|
14748
15304
|
|
|
15305
|
+
## Motion
|
|
15306
|
+
|
|
15307
|
+
- **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.
|
|
15308
|
+
- **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.
|
|
15309
|
+
- **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.
|
|
15310
|
+
- **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`.
|
|
15311
|
+
|
|
14749
15312
|
## Accessibility
|
|
14750
15313
|
|
|
14751
15314
|
- Default rows are presentational layout; semantics come from the content you place in title/subtitle/action.
|
|
@@ -14781,7 +15344,7 @@ import { Avatar, Entity } from "@sableui/react"
|
|
|
14781
15344
|
|
|
14782
15345
|
**Status:** stable
|
|
14783
15346
|
|
|
14784
|
-
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.
|
|
15347
|
+
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.
|
|
14785
15348
|
|
|
14786
15349
|
**Keywords:** feedback, sentiment, rating, prompt
|
|
14787
15350
|
|
|
@@ -14805,11 +15368,21 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
|
|
|
14805
15368
|
| placeholder | string | "What can we improve?" | Placeholder for the message field. |
|
|
14806
15369
|
| submitLabel | string | "Send" | Submit button text. |
|
|
14807
15370
|
| ratings | FeedbackRating[] | 🙁 / 😐 / 🙂 | The rating options shown as a row of toggles. Each is { value, label }. |
|
|
14808
|
-
| onSubmit | (data: { rating?: string; message: string }) => void | — | Called with the selected rating + message when the form is submitted. |
|
|
15371
|
+
| 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. |
|
|
15372
|
+
| 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. |
|
|
15373
|
+
| 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. |
|
|
14809
15374
|
|
|
14810
15375
|
## Tokens used
|
|
14811
15376
|
|
|
14812
|
-
`popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
|
|
15377
|
+
`popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`
|
|
15378
|
+
|
|
15379
|
+
## Motion
|
|
15380
|
+
|
|
15381
|
+
- **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.
|
|
15382
|
+
- **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.
|
|
15383
|
+
- **response** · Chosen rating mark · `motion-bloom` — The mark itself answers the pick: it blooms past its size and settles, so choosing reads as a small decision landing rather than a fill changing. Only the chosen mark, only on choosing — nothing blooms when the form opens or a pick is let go. Expressive: under subtle the fill alone answers.
|
|
15384
|
+
- **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.
|
|
15385
|
+
- **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.
|
|
14813
15386
|
|
|
14814
15387
|
## Accessibility
|
|
14815
15388
|
|
|
@@ -14818,6 +15391,8 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
|
|
|
14818
15391
|
- The form is named by its heading (aria-labelledby), so it is announced as "Send feedback" rather than an unnamed form.
|
|
14819
15392
|
- The message field carries an aria-label matching its placeholder.
|
|
14820
15393
|
- Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer.
|
|
15394
|
+
- 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.
|
|
15395
|
+
- The thanks dwell (about 1.2s) is a reading time, not motion, so it is the same under reduced motion.
|
|
14821
15396
|
|
|
14822
15397
|
## Related
|
|
14823
15398
|
|
|
@@ -14841,6 +15416,14 @@ import { Feedback } from "@sableui/react"
|
|
|
14841
15416
|
<Feedback label="Tell us" submitLabel="Send it" placeholder="Describe the issue…" />
|
|
14842
15417
|
```
|
|
14843
15418
|
|
|
15419
|
+
### Custom thanks
|
|
15420
|
+
|
|
15421
|
+
```tsx
|
|
15422
|
+
import { Feedback } from "@sableui/react"
|
|
15423
|
+
|
|
15424
|
+
<Feedback successMessage="Got it — thank you." onSubmit={({ rating, message }) => report(rating, message)} />
|
|
15425
|
+
```
|
|
15426
|
+
|
|
14844
15427
|
---
|
|
14845
15428
|
|
|
14846
15429
|
# Fieldset
|
|
@@ -15724,6 +16307,13 @@ Use InputGroup when something belongs inside the field with the text: a unit, a
|
|
|
15724
16307
|
|
|
15725
16308
|
`input`, `card`, `ring`, `destructive`, `muted-foreground`, `foreground`
|
|
15726
16309
|
|
|
16310
|
+
## Motion
|
|
16311
|
+
|
|
16312
|
+
- **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.
|
|
16313
|
+
- **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.
|
|
16314
|
+
- **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.
|
|
16315
|
+
- **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.
|
|
16316
|
+
|
|
15727
16317
|
## Accessibility
|
|
15728
16318
|
|
|
15729
16319
|
- The Input stays the focusable control; the group draws the focus ring around the whole field while it is focused.
|
|
@@ -15758,7 +16348,7 @@ import { Field, Icon, Input, InputGroup } from "@sableui/react"
|
|
|
15758
16348
|
```tsx
|
|
15759
16349
|
import { Button, Field, Icon, Input, InputGroup } from "@sableui/react"
|
|
15760
16350
|
|
|
15761
|
-
<Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><
|
|
16351
|
+
<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>
|
|
15762
16352
|
```
|
|
15763
16353
|
|
|
15764
16354
|
### Password with reveal
|
|
@@ -15919,32 +16509,126 @@ import { Label } from "@sableui/react"
|
|
|
15919
16509
|
|
|
15920
16510
|
---
|
|
15921
16511
|
|
|
15922
|
-
#
|
|
16512
|
+
# LikeButton
|
|
15923
16513
|
|
|
15924
|
-
**Status:**
|
|
16514
|
+
**Status:** stable
|
|
15925
16515
|
|
|
15926
|
-
|
|
16516
|
+
A pressed-or-not toggle for a like, a star or a save, with its count. Pressing it on fills the glyph, which blooms while a ring and a spray of dots burst from it and the count rolls up; pressing it off is quiet.
|
|
15927
16517
|
|
|
15928
|
-
**Keywords:**
|
|
16518
|
+
**Keywords:** like, heart, favorite, favourite, love, star, bookmark, save, upvote, reaction, count, toggle
|
|
15929
16519
|
|
|
15930
16520
|
## Usage
|
|
15931
16521
|
|
|
15932
|
-
Use
|
|
16522
|
+
Use LikeButton for a personal, reversible endorsement that other people can see the tally of: liking a post, starring a repository, saving a recipe. It is the one place in Sable that celebrates, because the person chose it and it costs nothing to undo.
|
|
15933
16523
|
|
|
15934
16524
|
### Best practices
|
|
15935
16525
|
|
|
15936
|
-
- Do:
|
|
15937
|
-
- Do:
|
|
15938
|
-
-
|
|
15939
|
-
- Don't:
|
|
16526
|
+
- Do: Pass the server's total as `count` and update it when the save lands. The button shows the viewer's own like at once, before the save, and goes back to `count` exactly whenever it changes, so a failed save that leaves `count` alone can also turn `pressed` back off.
|
|
16527
|
+
- Do: Match the glyph, family and label to the meaning: `icon="star" tone="yellow" label="Star"` for a star. The label is the accessible name, so it must say what pressing does.
|
|
16528
|
+
- Do: Use `format={{ notation: "compact" }}` once counts pass a few thousand, so a feed of buttons keeps one width.
|
|
16529
|
+
- Don't: Do not use it for a setting or a view switch (bold, mute, grid or list). Those are tools, not endorsements, and a burst on every press of a formatting control is noise; use Toggle. → use **Toggle**
|
|
16530
|
+
- Don't: Do not use it for a rating or a verdict on an answer (helpful, not helpful). That is a question with two sides; use Feedback. → use **Feedback**
|
|
16531
|
+
- Don't: Do not press it for the user. A burst is a reply to a person's choice — liking something on their behalf, or on load, turns delight into a notification.
|
|
15940
16532
|
|
|
15941
16533
|
## Props
|
|
15942
16534
|
|
|
15943
16535
|
| Name | Type | Default | Description |
|
|
15944
16536
|
| --- | --- | --- | --- |
|
|
15945
|
-
|
|
|
15946
|
-
|
|
|
15947
|
-
|
|
|
16537
|
+
| count | number | — | The total, rolled when it changes. Pressing adds or removes the viewer's like at once; the button shows `count` exactly again whenever `count` changes. Omit it for a bare glyph (an icon-size button). |
|
|
16538
|
+
| pressed | boolean | — | Controlled pressed state. |
|
|
16539
|
+
| defaultPressed | boolean | false | Initial pressed state when uncontrolled. |
|
|
16540
|
+
| onPressedChange | (pressed: boolean, event) => void | — | Called when the user presses it, with the new state. |
|
|
16541
|
+
| icon | string | "heart" | The glyph, by icon name. Its `solid` variant is shown while pressed. |
|
|
16542
|
+
| tone | PaletteColor | "red" | The palette family the glyph fills with while pressed, and the burst blooms from. A family, so it follows the theme. |
|
|
16543
|
+
| label | string | "Like" | The accessible name, read before the count. |
|
|
16544
|
+
| format | Intl.NumberFormatOptions | — | How to write the count, e.g. `{ notation: "compact" }`. |
|
|
16545
|
+
| variant | "ghost" \| "outline" \| "secondary" | "ghost" | The Button surface. |
|
|
16546
|
+
| size | "xs" \| "sm" \| "md" | "sm" | A Button size; without a count, the matching icon size. |
|
|
16547
|
+
| disabled | boolean | false | Disables it. |
|
|
16548
|
+
| 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 bloom and the burst are the flourish; the fill and the count changing stay. |
|
|
16549
|
+
|
|
16550
|
+
## Variants
|
|
16551
|
+
|
|
16552
|
+
- **variant**: ghost, outline, secondary
|
|
16553
|
+
- **size**: xs, sm, md
|
|
16554
|
+
- **tone**: red, orange, yellow, green, teal, blue, purple, pink
|
|
16555
|
+
|
|
16556
|
+
## Tokens used
|
|
16557
|
+
|
|
16558
|
+
`palette-dot`, `accent`, `ring`
|
|
16559
|
+
|
|
16560
|
+
## Motion
|
|
16561
|
+
|
|
16562
|
+
- **arrival** · Root · _no motion_ — It arrives with the post or row it belongs to. A page that loads with likes already given must be still: nothing blooms on mount.
|
|
16563
|
+
- **response** · Glyph · `motion-bloom` — Pressing it on is the moment. The glyph is 16px, so it needs real amplitude to read as an event: it grows from nothing past its size and settles, after a 90ms beat in which the ring leads. Inherits Button's motion-press for the press itself. Pressing off has no bloom — taking a like back is not a celebration.
|
|
16564
|
+
- **transition** · Burst · _custom (outside the recipe layer)_ — The follow-through: <Burst> draws a disc that hollows into a ring and shifts colour, then dots thrown outward in the theme's palette families. Expressive only — under `subtle` or reduced motion it renders nothing and the fill alone answers the press. The count rolls with NumberTicker.
|
|
16565
|
+
- **activity** · Root · _no motion_ — There is no in-between state to show: the like is shown at once, before it is saved.
|
|
16566
|
+
|
|
16567
|
+
## Accessibility
|
|
16568
|
+
|
|
16569
|
+
- A native <button> through Button, with `aria-pressed`. Enter and Space press it.
|
|
16570
|
+
- The accessible name is the label then the count ("Like 1,301"); the burst is aria-hidden and the rolling digits are hidden behind the count's real text.
|
|
16571
|
+
- The pressed state is carried by the filled glyph and `aria-pressed`, never by colour alone.
|
|
16572
|
+
- Under reduced motion the glyph fills and the count changes at once; the bloom and the burst are dropped.
|
|
16573
|
+
|
|
16574
|
+
## Related
|
|
16575
|
+
|
|
16576
|
+
- Components: Toggle, Feedback, NumberTicker, Button
|
|
16577
|
+
|
|
16578
|
+
## Examples
|
|
16579
|
+
|
|
16580
|
+
### A post's likes
|
|
16581
|
+
|
|
16582
|
+
```tsx
|
|
16583
|
+
import { LikeButton } from "@sableui/react"
|
|
16584
|
+
|
|
16585
|
+
<LikeButton count={post.likes} defaultPressed={post.liked} onPressedChange={(on) => api.like(post.id, on)} />
|
|
16586
|
+
```
|
|
16587
|
+
|
|
16588
|
+
### Star a repository
|
|
16589
|
+
|
|
16590
|
+
```tsx
|
|
16591
|
+
import { LikeButton } from "@sableui/react"
|
|
16592
|
+
|
|
16593
|
+
<LikeButton icon="star" tone="yellow" label="Star" variant="outline" count={repo.stars} format={{ notation: "compact" }} />
|
|
16594
|
+
```
|
|
16595
|
+
|
|
16596
|
+
### Bare glyph
|
|
16597
|
+
|
|
16598
|
+
```tsx
|
|
16599
|
+
import { LikeButton } from "@sableui/react"
|
|
16600
|
+
|
|
16601
|
+
<LikeButton label="Save recipe" />
|
|
16602
|
+
```
|
|
16603
|
+
|
|
16604
|
+
---
|
|
16605
|
+
|
|
16606
|
+
# LineChart
|
|
16607
|
+
|
|
16608
|
+
**Status:** beta
|
|
16609
|
+
|
|
16610
|
+
Lines over a sequence — for comparing how measures move. Smooth, linear or step curves, optional dots (coloured by series or by category) and value labels. Lines draw on as the chart scrolls into view.
|
|
16611
|
+
|
|
16612
|
+
**Keywords:** chart, line, trend, time series, graph, compare, analytics, dashboard
|
|
16613
|
+
|
|
16614
|
+
## Usage
|
|
16615
|
+
|
|
16616
|
+
Use LineChart to compare how two or more measures change over the same sequence, or to show one trend precisely (with dots and labels). Unlike AreaChart, its value axis is fitted to the data rather than forced to zero, so small movements stay visible — say so if the axis is shown.
|
|
16617
|
+
|
|
16618
|
+
### Best practices
|
|
16619
|
+
|
|
16620
|
+
- Do: Turn on dots and labels for six or fewer points, when the exact values are the story.
|
|
16621
|
+
- Do: Use curve="step" for values that hold until they change — prices, plan tiers, headcount.
|
|
16622
|
+
- Don't: Plot unordered categories (channels, products) as a line — the line implies a path between them; use BarChart.
|
|
16623
|
+
- Don't: Use it for data-heavy views — streaming or real-time data, thousands of points, zoom and brushing, trading charts. Those belong on a charting library (Recharts, visx, ECharts); Sable's charts are for dashboards, landing pages and decks.
|
|
16624
|
+
|
|
16625
|
+
## Props
|
|
16626
|
+
|
|
16627
|
+
| Name | Type | Default | Description |
|
|
16628
|
+
| --- | --- | --- | --- |
|
|
16629
|
+
| data | Record<string, unknown>[] | — | The rows, in order along the category axis. |
|
|
16630
|
+
| index | string | — | The datum field naming each row's category (a month, a day, a channel). |
|
|
16631
|
+
| series | { key: string; label: string; tone?: ChartTone; color?: string; icon?: string }[] | — | The measures to plot: the datum field, its name in the legend/tooltip/table, and its colour (a tone by role, or any CSS colour). Omitted, series take the chart palette in order: --chart-1 … --chart-8, validated for colour-blind separation in light and dark; a theme may override the set. |
|
|
15948
16632
|
| label | string | — | What the chart shows. Its accessible name and the caption of the data table assistive tech reads. |
|
|
15949
16633
|
| className | string | "h-60" | Sizes the chart (plot + legend). Set a height: `h-72`, `aspect-video h-auto`. |
|
|
15950
16634
|
| valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
|
|
@@ -15955,7 +16639,8 @@ Use LineChart to compare how two or more measures change over the same sequence,
|
|
|
15955
16639
|
| grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
|
|
15956
16640
|
| xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
|
|
15957
16641
|
| yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
|
|
15958
|
-
|
|
|
16642
|
+
| 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. |
|
|
16643
|
+
| animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`. |
|
|
15959
16644
|
| activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
|
|
15960
16645
|
| defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
|
|
15961
16646
|
| onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
|
|
@@ -15970,9 +16655,9 @@ Use LineChart to compare how two or more measures change over the same sequence,
|
|
|
15970
16655
|
|
|
15971
16656
|
## Motion
|
|
15972
16657
|
|
|
15973
|
-
- **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.
|
|
16658
|
+
- **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.
|
|
15974
16659
|
- **response** · Cursor + tooltip · _no motion_ — Hover and focus move the cursor and tooltip instantly — a lagging cursor reads as a slow chart.
|
|
15975
|
-
- **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.
|
|
16660
|
+
- **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.
|
|
15976
16661
|
- **activity** · LineChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
|
|
15977
16662
|
|
|
15978
16663
|
## Accessibility
|
|
@@ -17144,7 +17829,7 @@ import { MiddleTruncate } from "@sableui/react"
|
|
|
17144
17829
|
import { Button, Icon, MiddleTruncate, Row, useCopy } from "@sableui/react"
|
|
17145
17830
|
|
|
17146
17831
|
const { copy, copied } = useCopy(commitHash)
|
|
17147
|
-
<Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><Icon name={copied
|
|
17832
|
+
<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>
|
|
17148
17833
|
```
|
|
17149
17834
|
|
|
17150
17835
|
---
|
|
@@ -17304,6 +17989,14 @@ Use Note for helpful supporting information that should sit near the relevant co
|
|
|
17304
17989
|
|
|
17305
17990
|
`foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`
|
|
17306
17991
|
|
|
17992
|
+
## Motion
|
|
17993
|
+
|
|
17994
|
+
- **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.
|
|
17995
|
+
- **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.
|
|
17996
|
+
- **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.
|
|
17997
|
+
- **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.
|
|
17998
|
+
- **activity** · Root · _no motion_ — Static guidance. Work in progress belongs in a Progress or a Spinner, not in a callout.
|
|
17999
|
+
|
|
17307
18000
|
## Accessibility
|
|
17308
18001
|
|
|
17309
18002
|
- Rendered with role="note" so assistive tech can announce it as an aside.
|
|
@@ -17353,7 +18046,7 @@ import { Note } from "@sableui/react"
|
|
|
17353
18046
|
|
|
17354
18047
|
**Status:** stable
|
|
17355
18048
|
|
|
17356
|
-
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.
|
|
18049
|
+
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.
|
|
17357
18050
|
|
|
17358
18051
|
**Keywords:** notification, badge, count, unread
|
|
17359
18052
|
|
|
@@ -17379,6 +18072,7 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
|
|
|
17379
18072
|
| tone | "destructive" \| "primary" \| "neutral" | "destructive" | Fill color. destructive for alarming counts; primary/neutral for non-alarming ones. |
|
|
17380
18073
|
| 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). |
|
|
17381
18074
|
| 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. |
|
|
18075
|
+
| 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. |
|
|
17382
18076
|
|
|
17383
18077
|
## Variants
|
|
17384
18078
|
|
|
@@ -17390,6 +18084,13 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
|
|
|
17390
18084
|
|
|
17391
18085
|
`destructive`, `destructive-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`
|
|
17392
18086
|
|
|
18087
|
+
## Motion
|
|
18088
|
+
|
|
18089
|
+
- **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.
|
|
18090
|
+
- **response** · root · _no motion_ — It is decorative and not interactive; the host it sits on answers the pointer.
|
|
18091
|
+
- **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.
|
|
18092
|
+
- **activity** · root · _no motion_ — An unread count has no working state.
|
|
18093
|
+
|
|
17393
18094
|
## Accessibility
|
|
17394
18095
|
|
|
17395
18096
|
- 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.
|
|
@@ -17397,7 +18098,7 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
|
|
|
17397
18098
|
|
|
17398
18099
|
## Related
|
|
17399
18100
|
|
|
17400
|
-
- Components: Badge, StatusDot
|
|
18101
|
+
- Components: Badge, StatusDot, NumberTicker
|
|
17401
18102
|
|
|
17402
18103
|
## Examples
|
|
17403
18104
|
|
|
@@ -17492,6 +18193,95 @@ import { NumberField } from "@sableui/react"
|
|
|
17492
18193
|
|
|
17493
18194
|
---
|
|
17494
18195
|
|
|
18196
|
+
# NumberTicker
|
|
18197
|
+
|
|
18198
|
+
**Status:** stable
|
|
18199
|
+
|
|
18200
|
+
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.
|
|
18201
|
+
|
|
18202
|
+
**Keywords:** number, counter, odometer, rolling number, animated number, ticker, count up, stat, kpi, metric, price
|
|
18203
|
+
|
|
18204
|
+
## Usage
|
|
18205
|
+
|
|
18206
|
+
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.
|
|
18207
|
+
|
|
18208
|
+
### Best practices
|
|
18209
|
+
|
|
18210
|
+
- 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.
|
|
18211
|
+
- Do: Format through `format` (currency, percent, compact) rather than pre-formatting a string, so separators and units stay put while only the digits roll.
|
|
18212
|
+
- 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.
|
|
18213
|
+
- 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.
|
|
18214
|
+
- 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.
|
|
18215
|
+
- 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).
|
|
18216
|
+
- 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**
|
|
18217
|
+
|
|
18218
|
+
## Props
|
|
18219
|
+
|
|
18220
|
+
| Name | Type | Default | Description |
|
|
18221
|
+
| --- | --- | --- | --- |
|
|
18222
|
+
| value | number | — | The number to show. When it changes, the digits roll to it. |
|
|
18223
|
+
| 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). |
|
|
18224
|
+
| locales | string \| string[] | runtime locale | BCP 47 locale(s) for the formatting. Digits roll through the locale's own numbering system. |
|
|
18225
|
+
| 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. |
|
|
18226
|
+
| suffix | ReactNode | — | After the number ("+", " pts"). A string becomes part of the number's text; an element is rendered as given. |
|
|
18227
|
+
| 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). |
|
|
18228
|
+
| 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. |
|
|
18229
|
+
| 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. |
|
|
18230
|
+
| ...span props | Omit<ComponentPropsWithRef<"span">, "children" \| "prefix"> | — | Remaining span attributes pass through to the root — `aria-live` to announce changes, `id`, data attributes. |
|
|
18231
|
+
|
|
18232
|
+
## Motion
|
|
18233
|
+
|
|
18234
|
+
- **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.
|
|
18235
|
+
- **response** · Root · _no motion_ — It is a reading, not a control; the control that changes it gives the feedback.
|
|
18236
|
+
- **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.
|
|
18237
|
+
- **activity** · Root · _no motion_ — A number has no working state; show a Skeleton while it loads.
|
|
18238
|
+
|
|
18239
|
+
## Accessibility
|
|
18240
|
+
|
|
18241
|
+
- 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.
|
|
18242
|
+
- 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.
|
|
18243
|
+
- Tabular figures keep every digit the same width, so a roll never shifts the text around it.
|
|
18244
|
+
|
|
18245
|
+
## Related
|
|
18246
|
+
|
|
18247
|
+
- Components: Meter, NotificationBadge, PieChart, RadialChart, Sparkline
|
|
18248
|
+
|
|
18249
|
+
## Examples
|
|
18250
|
+
|
|
18251
|
+
### Basic
|
|
18252
|
+
|
|
18253
|
+
```tsx
|
|
18254
|
+
import { NumberTicker } from "@sableui/react"
|
|
18255
|
+
|
|
18256
|
+
<NumberTicker value={1284} className="text-heading-2" />
|
|
18257
|
+
```
|
|
18258
|
+
|
|
18259
|
+
### Currency
|
|
18260
|
+
|
|
18261
|
+
```tsx
|
|
18262
|
+
import { NumberTicker } from "@sableui/react"
|
|
18263
|
+
|
|
18264
|
+
<NumberTicker value={4280.5} format={{ style: "currency", currency: "USD" }} className="text-display-lg" />
|
|
18265
|
+
```
|
|
18266
|
+
|
|
18267
|
+
### Percent, announced
|
|
18268
|
+
|
|
18269
|
+
```tsx
|
|
18270
|
+
import { NumberTicker } from "@sableui/react"
|
|
18271
|
+
|
|
18272
|
+
<NumberTicker value={0.428} format={{ style: "percent", maximumFractionDigits: 1 }} aria-live="polite" />
|
|
18273
|
+
```
|
|
18274
|
+
|
|
18275
|
+
### Subtle
|
|
18276
|
+
|
|
18277
|
+
```tsx
|
|
18278
|
+
import { NumberTicker } from "@sableui/react"
|
|
18279
|
+
|
|
18280
|
+
<NumberTicker value={92} suffix=" pts" motion="subtle" />
|
|
18281
|
+
```
|
|
18282
|
+
|
|
18283
|
+
---
|
|
18284
|
+
|
|
17495
18285
|
# OtpField
|
|
17496
18286
|
|
|
17497
18287
|
**Status:** stable
|
|
@@ -17509,6 +18299,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
|
|
|
17509
18299
|
- 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.
|
|
17510
18300
|
- 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.
|
|
17511
18301
|
- 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.
|
|
18302
|
+
- 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.
|
|
17512
18303
|
- 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.
|
|
17513
18304
|
- 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.
|
|
17514
18305
|
- 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.
|
|
@@ -17535,7 +18326,8 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
|
|
|
17535
18326
|
| autoSubmit | boolean | false | Submit the owning form when the code becomes complete. |
|
|
17536
18327
|
| mask | boolean | false | Render slots as password inputs. Only for codes that are secret on screen, such as a card PIN. |
|
|
17537
18328
|
| getSlotLabel | (index: number, length: number) => string | Character N of L | Accessible name for slots after the first. Override to localize. |
|
|
17538
|
-
| aria-invalid | boolean | — | Marks every slot invalid (destructive border). `<Field error>` sets it for you. |
|
|
18329
|
+
| 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. |
|
|
18330
|
+
| 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. |
|
|
17539
18331
|
| disabled / readOnly / required / name / id / form | — | — | Standard form-control props. `id` lands on the first slot, so `<Field>` and `<label htmlFor>` name the field. |
|
|
17540
18332
|
|
|
17541
18333
|
## Tokens used
|
|
@@ -17547,6 +18339,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
|
|
|
17547
18339
|
- **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.
|
|
17548
18340
|
- **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.
|
|
17549
18341
|
- **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.
|
|
18342
|
+
- **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.
|
|
17550
18343
|
- **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.
|
|
17551
18344
|
|
|
17552
18345
|
## Accessibility
|
|
@@ -17555,6 +18348,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
|
|
|
17555
18348
|
- Only one slot is in the tab order. Arrow keys, Home/End and Backspace move between slots; Tab leaves the whole field.
|
|
17556
18349
|
- The first slot carries autocomplete="one-time-code" and the numeric virtual keyboard, so iOS and Android offer the SMS code above the keyboard.
|
|
17557
18350
|
- Slots render at 20px (`text-mono-lg`), above the 16px floor that stops iOS Safari zooming on focus.
|
|
18351
|
+
- 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`.
|
|
17558
18352
|
|
|
17559
18353
|
## Related
|
|
17560
18354
|
|
|
@@ -17586,6 +18380,16 @@ import { Field, OtpField } from "@sableui/react"
|
|
|
17586
18380
|
<Field label="Verification code" error="That code has expired. Request a new code and try again."><OtpField length={6} groupSize={3} defaultValue="482913" /></Field>
|
|
17587
18381
|
```
|
|
17588
18382
|
|
|
18383
|
+
### Verify on completion, shake on a wrong code
|
|
18384
|
+
|
|
18385
|
+
```tsx
|
|
18386
|
+
import { Field, OtpField } from "@sableui/react"
|
|
18387
|
+
|
|
18388
|
+
const [code, setCode] = useState("")
|
|
18389
|
+
const [wrong, setWrong] = useState(false)
|
|
18390
|
+
<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>
|
|
18391
|
+
```
|
|
18392
|
+
|
|
17589
18393
|
### Alphanumeric
|
|
17590
18394
|
|
|
17591
18395
|
```tsx
|
|
@@ -17986,6 +18790,13 @@ Use PanelGroup for the body of a tool workspace: fixed-width Panels docked left
|
|
|
17986
18790
|
|
|
17987
18791
|
`primary`, `panel`, `hairline`, `hairline-strong`
|
|
17988
18792
|
|
|
18793
|
+
## Motion
|
|
18794
|
+
|
|
18795
|
+
- **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.
|
|
18796
|
+
- **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.
|
|
18797
|
+
- **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.
|
|
18798
|
+
- **activity** · Root · _no motion_ — Layout chrome has no working state.
|
|
18799
|
+
|
|
17989
18800
|
## Accessibility
|
|
17990
18801
|
|
|
17991
18802
|
- PanelResizer is a role=separator with aria-orientation, aria-valuenow/min/max, and is focusable — a pointer-only resizer is a keyboard trap.
|
|
@@ -18118,6 +18929,14 @@ Use PanelSection to group properties inside an inspector or docked panel, with P
|
|
|
18118
18929
|
|
|
18119
18930
|
`foreground`, `muted-foreground`, `hairline`
|
|
18120
18931
|
|
|
18932
|
+
## Motion
|
|
18933
|
+
|
|
18934
|
+
- **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.
|
|
18935
|
+
- **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.
|
|
18936
|
+
- **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.
|
|
18937
|
+
- **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.
|
|
18938
|
+
- **activity** · Root · _no motion_ — The inspector edits values in place; nothing in it loads.
|
|
18939
|
+
|
|
18121
18940
|
## Accessibility
|
|
18122
18941
|
|
|
18123
18942
|
- 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.
|
|
@@ -18332,7 +19151,8 @@ Use PieChart for a share of one total across two to six categories — traffic s
|
|
|
18332
19151
|
| 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. |
|
|
18333
19152
|
| valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
|
|
18334
19153
|
| locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
|
|
18335
|
-
|
|
|
19154
|
+
| 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. |
|
|
19155
|
+
| animate | boolean | true | Sweep the slices in clockwise when the chart scrolls into view. `animate={false}` is `motion="subtle"`. |
|
|
18336
19156
|
| activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
|
|
18337
19157
|
| defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
|
|
18338
19158
|
| onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
|
|
@@ -18343,9 +19163,10 @@ Use PieChart for a share of one total across two to six categories — traffic s
|
|
|
18343
19163
|
|
|
18344
19164
|
## Motion
|
|
18345
19165
|
|
|
18346
|
-
- **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.
|
|
18347
|
-
- **response** · Active slice · _no motion_ — The hovered slice pulls outward and the rest dim at once; the geometry change itself eases (see transition).
|
|
18348
|
-
- **transition** ·
|
|
19166
|
+
- **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.
|
|
19167
|
+
- **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.
|
|
19168
|
+
- **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.
|
|
19169
|
+
- **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.
|
|
18349
19170
|
- **activity** · PieChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
|
|
18350
19171
|
|
|
18351
19172
|
## Accessibility
|
|
@@ -18441,6 +19262,14 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
18441
19262
|
|
|
18442
19263
|
`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`
|
|
18443
19264
|
|
|
19265
|
+
## Motion
|
|
19266
|
+
|
|
19267
|
+
- **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.
|
|
19268
|
+
- **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.
|
|
19269
|
+
- **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.
|
|
19270
|
+
- **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.
|
|
19271
|
+
- **activity** · Root · _no motion_ — A chip is a choice, not a process. If applying it is slow, the results show it.
|
|
19272
|
+
|
|
18444
19273
|
## Accessibility
|
|
18445
19274
|
|
|
18446
19275
|
- Renders a native <button type="button"> for focus and keyboard activation.
|
|
@@ -18706,7 +19535,8 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
|
|
|
18706
19535
|
| 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. |
|
|
18707
19536
|
| valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
|
|
18708
19537
|
| locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
|
|
18709
|
-
|
|
|
19538
|
+
| 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. |
|
|
19539
|
+
| animate | boolean | true | Grow the polygons from the centre when the chart scrolls into view. `animate={false}` is `motion="subtle"`. |
|
|
18710
19540
|
| activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
|
|
18711
19541
|
| defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
|
|
18712
19542
|
| onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
|
|
@@ -18717,9 +19547,9 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
|
|
|
18717
19547
|
|
|
18718
19548
|
## Motion
|
|
18719
19549
|
|
|
18720
|
-
- **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.
|
|
19550
|
+
- **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.
|
|
18721
19551
|
- **response** · Active spoke · _no motion_ — The hovered spoke's vertices and label highlight at once.
|
|
18722
|
-
- **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.
|
|
19552
|
+
- **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.
|
|
18723
19553
|
- **activity** · RadarChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
|
|
18724
19554
|
|
|
18725
19555
|
## Accessibility
|
|
@@ -18817,7 +19647,8 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
|
|
|
18817
19647
|
| 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. |
|
|
18818
19648
|
| valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
|
|
18819
19649
|
| locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
|
|
18820
|
-
|
|
|
19650
|
+
| 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. |
|
|
19651
|
+
| animate | boolean | true | Draw the rings round from their start when the chart scrolls into view. `animate={false}` is `motion="subtle"`. |
|
|
18821
19652
|
| activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
|
|
18822
19653
|
| defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
|
|
18823
19654
|
| onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
|
|
@@ -18828,9 +19659,10 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
|
|
|
18828
19659
|
|
|
18829
19660
|
## Motion
|
|
18830
19661
|
|
|
18831
|
-
- **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.
|
|
18832
|
-
- **response** · Active ring · _no motion_ — The hovered ring stays full strength and the others dim at once.
|
|
18833
|
-
- **transition** ·
|
|
19662
|
+
- **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.
|
|
19663
|
+
- **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.
|
|
19664
|
+
- **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.
|
|
19665
|
+
- **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.
|
|
18834
19666
|
- **activity** · RadialChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
|
|
18835
19667
|
|
|
18836
19668
|
## Accessibility
|
|
@@ -18925,6 +19757,13 @@ Use Radio when a user must choose exactly one option from a visible set.
|
|
|
18925
19757
|
|
|
18926
19758
|
`input`, `card`, `primary`, `ring`
|
|
18927
19759
|
|
|
19760
|
+
## Motion
|
|
19761
|
+
|
|
19762
|
+
- **arrival** · Root · _no motion_ — Radios arrive with the form they belong to. Animating each one in turns a question into a title sequence.
|
|
19763
|
+
- **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.
|
|
19764
|
+
- **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.
|
|
19765
|
+
- **activity** · Root · _no motion_ — A radio commits instantly. A slow write belongs to the surrounding form's pending state, not the control.
|
|
19766
|
+
|
|
18928
19767
|
## Accessibility
|
|
18929
19768
|
|
|
18930
19769
|
- Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.
|
|
@@ -19026,6 +19865,15 @@ Use RadioCard for visual choice cards when users compare a small set of named op
|
|
|
19026
19865
|
|
|
19027
19866
|
`input`, `card`, `foreground`, `primary`, `muted`, `ring`, `muted-foreground`, `tone-3`, `tone-2`, `tone-1`, `tone-info`, `tone-warning`
|
|
19028
19867
|
|
|
19868
|
+
## Motion
|
|
19869
|
+
|
|
19870
|
+
- **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.
|
|
19871
|
+
- **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.
|
|
19872
|
+
- **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.
|
|
19873
|
+
- **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.
|
|
19874
|
+
- **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.
|
|
19875
|
+
- **activity** · Root · _no motion_ — A choice commits instantly. A slow write belongs to the surrounding form's pending state.
|
|
19876
|
+
|
|
19029
19877
|
## Accessibility
|
|
19030
19878
|
|
|
19031
19879
|
- Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.
|
|
@@ -19066,6 +19914,124 @@ import { RadioCard, RadioGroup } from "@sableui/react"
|
|
|
19066
19914
|
|
|
19067
19915
|
---
|
|
19068
19916
|
|
|
19917
|
+
# ReactionPicker
|
|
19918
|
+
|
|
19919
|
+
**Status:** stable
|
|
19920
|
+
|
|
19921
|
+
The quick row of emoji you react to a message with: a toolbar of large emoji toggles that bloom in one after another as it opens and magnify under the pointer, with MenuReactions as the same row inside a right-click menu.
|
|
19922
|
+
|
|
19923
|
+
**Keywords:** reaction, reactions, emoji, emoji picker, react, tapback, quick reactions, message actions, chat
|
|
19924
|
+
|
|
19925
|
+
## Usage
|
|
19926
|
+
|
|
19927
|
+
Use ReactionPicker wherever a reader chooses a reaction for something: in a Popover from an add-reaction control (BubbleReactionAdd is that, ready-made), or as a hover bar over a message. Use MenuReactions for the same row as the first group of the menu a message opens on right-click or long-press, above Reply and Copy. Both report which emoji the reader has already used, so choosing one again removes it.
|
|
19928
|
+
|
|
19929
|
+
### Best practices
|
|
19930
|
+
|
|
19931
|
+
- Do: Keep the quick set short — six to eight. It is the reactions people actually use; a full emoji catalogue belongs behind a trailing "more" control passed as children.
|
|
19932
|
+
- Do: Give every reaction a `label`. It is the accessible name and the menu's typeahead, and an emoji's own spoken name differs between platforms.
|
|
19933
|
+
- Do: Pass `selected` so the reader's own reactions show pressed, and toggle in `onSelect`: choosing a reaction you already gave takes it back.
|
|
19934
|
+
- Do: Offer the right-click menu and a visible add-reaction control together. A context menu is invisible until you know it is there; the pill is how a first-time reader finds reactions.
|
|
19935
|
+
- Don't: Do not put a ReactionPicker inside a Menu or ContextMenu. A toolbar is not a menu item, so the menu's arrow keys would skip it; use MenuReactions there. → use **MenuReactions**
|
|
19936
|
+
- Don't: Do not use it to rate an answer or collect a verdict. Reactions are social and many-valued; a helpful / not-helpful question is Feedback. → use **Feedback**
|
|
19937
|
+
|
|
19938
|
+
### Anatomy
|
|
19939
|
+
|
|
19940
|
+
| Part | Required | Description |
|
|
19941
|
+
| --- | --- | --- |
|
|
19942
|
+
| ReactionPicker | yes | The toolbar: one tab stop, arrow keys move between emoji, each a toggle with `aria-pressed`. |
|
|
19943
|
+
| MenuReactions | no | The same row as a group of checkbox menu items, for MenuContent and ContextMenuContent. Choosing one closes the menu. |
|
|
19944
|
+
|
|
19945
|
+
## Subcomponents
|
|
19946
|
+
|
|
19947
|
+
| Name | Type | Description |
|
|
19948
|
+
| --- | --- | --- |
|
|
19949
|
+
| MenuReactions | { reactions?; selected?; onSelect?; motion? } & ComponentProps<typeof Menu.Group> | The quick row as checkbox menu items, for the first group of a MenuContent or ContextMenuContent. Same props as ReactionPicker, without children. |
|
|
19950
|
+
|
|
19951
|
+
## Props
|
|
19952
|
+
|
|
19953
|
+
| Name | Type | Default | Description |
|
|
19954
|
+
| --- | --- | --- | --- |
|
|
19955
|
+
| reactions | readonly { emoji: string; label: string }[] | DEFAULT_REACTIONS | The choices, in order. The default is 👍 ❤️ 😂 🎉 😮 😢 🙏. |
|
|
19956
|
+
| selected | readonly string[] | [] | The emoji the reader has already reacted with: shown pressed, and choosing one again removes it. |
|
|
19957
|
+
| onSelect | (emoji: string) => void | — | Called with the emoji the reader chose. Toggle it in or out of `selected`. |
|
|
19958
|
+
| children | ReactNode | — | ReactionPicker only: trailing controls after the emoji, such as a button that opens a full emoji picker. |
|
|
19959
|
+
| aria-label | string | "Reactions" | The name of the toolbar or menu group. |
|
|
19960
|
+
| motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps the press and drops the wave of emoji blooming in and the magnify, `off` stills it. It can only turn motion down. |
|
|
19961
|
+
|
|
19962
|
+
## Tokens used
|
|
19963
|
+
|
|
19964
|
+
`accent`, `primary`, `ring`
|
|
19965
|
+
|
|
19966
|
+
## Motion
|
|
19967
|
+
|
|
19968
|
+
- **arrival** · Glyph · `motion-bloom` — Opening the picker is the reader's own action, so the emoji answer it: each blooms in 24ms after the one before, a short wave from first to last. The surface it sits in (a Popover or a menu) arrives with its own motion-pop. Expressive only — under `subtle` the emoji are simply there.
|
|
19969
|
+
- **response** · Glyph · _custom (outside the recipe layer)_ — The emoji under the pointer or the keyboard highlight magnifies to 1.25 on a fast standard ease, so you can see which one a release will choose; each cell also carries motion-interactive and motion-press. The magnify rides an inner layer so it never fights the bloom over `scale`.
|
|
19970
|
+
- **transition** · Cell · `motion-interactive` — Pressed and not pressed differ by a tint, eased by motion-interactive. The celebration belongs to the reaction that lands under the message (BubbleReaction), not to the picker that chose it.
|
|
19971
|
+
- **activity** · Root · _no motion_ — Choosing is instant; there is no in-between state to show.
|
|
19972
|
+
|
|
19973
|
+
## Accessibility
|
|
19974
|
+
|
|
19975
|
+
- ReactionPicker is a Base UI toolbar: one tab stop, Left/Right move between emoji, Home/End jump to the ends.
|
|
19976
|
+
- Each emoji is a button named by its `label` and reporting `aria-pressed`, so a screen-reader user hears whether choosing it adds or removes their reaction. The glyph itself is aria-hidden.
|
|
19977
|
+
- MenuReactions renders checkbox menu items (`menuitemcheckbox` with `aria-checked`), reached by the menu's arrow keys and typeahead like every other row.
|
|
19978
|
+
- Under reduced motion the emoji appear at once and do not magnify; pressed state is carried by `aria-pressed` and a tint, never motion.
|
|
19979
|
+
|
|
19980
|
+
## Related
|
|
19981
|
+
|
|
19982
|
+
- Components: Bubble, ContextMenu, Popover, Toolbar, Feedback
|
|
19983
|
+
|
|
19984
|
+
## Examples
|
|
19985
|
+
|
|
19986
|
+
### In a popover
|
|
19987
|
+
|
|
19988
|
+
```tsx
|
|
19989
|
+
import { Button, Icon, Popover, PopoverContent, PopoverTrigger, ReactionPicker } from "@sableui/react"
|
|
19990
|
+
|
|
19991
|
+
<Popover>
|
|
19992
|
+
<PopoverTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Add reaction" />}>
|
|
19993
|
+
<Icon name="emoji" />
|
|
19994
|
+
</PopoverTrigger>
|
|
19995
|
+
<PopoverContent side="top" className="w-auto rounded-full p-1">
|
|
19996
|
+
<ReactionPicker selected={mine} onSelect={toggle} />
|
|
19997
|
+
</PopoverContent>
|
|
19998
|
+
</Popover>
|
|
19999
|
+
```
|
|
20000
|
+
|
|
20001
|
+
### In a right-click menu
|
|
20002
|
+
|
|
20003
|
+
```tsx
|
|
20004
|
+
import { Bubble, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, MenuReactions } from "@sableui/react"
|
|
20005
|
+
|
|
20006
|
+
<ContextMenu>
|
|
20007
|
+
<ContextMenuTrigger render={<Bubble />}>See you at 6.</ContextMenuTrigger>
|
|
20008
|
+
<ContextMenuContent>
|
|
20009
|
+
<MenuReactions selected={mine} onSelect={toggle} />
|
|
20010
|
+
<ContextMenuSeparator />
|
|
20011
|
+
<ContextMenuItem>Reply</ContextMenuItem>
|
|
20012
|
+
<ContextMenuItem>Copy text</ContextMenuItem>
|
|
20013
|
+
</ContextMenuContent>
|
|
20014
|
+
</ContextMenu>
|
|
20015
|
+
```
|
|
20016
|
+
|
|
20017
|
+
### A custom set
|
|
20018
|
+
|
|
20019
|
+
```tsx
|
|
20020
|
+
import { ReactionPicker } from "@sableui/react"
|
|
20021
|
+
|
|
20022
|
+
<ReactionPicker
|
|
20023
|
+
reactions={[
|
|
20024
|
+
{ emoji: "✅", label: "Done" },
|
|
20025
|
+
{ emoji: "👀", label: "Looking" },
|
|
20026
|
+
{ emoji: "🚀", label: "Shipped" },
|
|
20027
|
+
]}
|
|
20028
|
+
selected={mine}
|
|
20029
|
+
onSelect={toggle}
|
|
20030
|
+
/>
|
|
20031
|
+
```
|
|
20032
|
+
|
|
20033
|
+
---
|
|
20034
|
+
|
|
19069
20035
|
# RelativeTime
|
|
19070
20036
|
|
|
19071
20037
|
**Status:** stable
|
|
@@ -19097,6 +20063,14 @@ Use RelativeTime to show recent activity or timestamps in conversational relativ
|
|
|
19097
20063
|
|
|
19098
20064
|
`popover`, `popover-foreground`, `border`, `ring`
|
|
19099
20065
|
|
|
20066
|
+
## Motion
|
|
20067
|
+
|
|
20068
|
+
- **arrival** · Root · _no motion_ — It is a word in a line of text and arrives with that line.
|
|
20069
|
+
- **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.
|
|
20070
|
+
- **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.
|
|
20071
|
+
- **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.
|
|
20072
|
+
- **activity** · Root · _no motion_ — A timestamp has nothing to wait for.
|
|
20073
|
+
|
|
19100
20074
|
## Accessibility
|
|
19101
20075
|
|
|
19102
20076
|
- Renders a semantic <time> element with a machine-readable dateTime (ISO) attribute.
|
|
@@ -19172,6 +20146,14 @@ Use Scroller to create a contained overflow region for content that naturally ex
|
|
|
19172
20146
|
|
|
19173
20147
|
`ring`
|
|
19174
20148
|
|
|
20149
|
+
## Motion
|
|
20150
|
+
|
|
20151
|
+
- **arrival** · Root · _no motion_ — It is a region, not an event; its content arrives with whatever fills it.
|
|
20152
|
+
- **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.
|
|
20153
|
+
- **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.
|
|
20154
|
+
- **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.
|
|
20155
|
+
- **activity** · Root · _no motion_ — A scroll region does no work of its own. Content still loading inside it brings its own Skeleton or Spinner.
|
|
20156
|
+
|
|
19175
20157
|
## Accessibility
|
|
19176
20158
|
|
|
19177
20159
|
- 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.
|
|
@@ -19265,6 +20247,14 @@ Use ScrubField for numeric properties in tool chrome — an inspector, a propert
|
|
|
19265
20247
|
|
|
19266
20248
|
`foreground`, `muted-foreground`, `ring`, `panel-field`, `panel-field-hover`
|
|
19267
20249
|
|
|
20250
|
+
## Motion
|
|
20251
|
+
|
|
20252
|
+
- **arrival** · Root · _no motion_ — It arrives with the inspector row it sits in.
|
|
20253
|
+
- **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.
|
|
20254
|
+
- **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.
|
|
20255
|
+
- **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.
|
|
20256
|
+
- **activity** · Root · _no motion_ — Scrubbing is synchronous; there is nothing to wait for.
|
|
20257
|
+
|
|
19268
20258
|
## Accessibility
|
|
19269
20259
|
|
|
19270
20260
|
- The input carries the accessible name via `label`; the glyph is aria-hidden because it is decorative.
|
|
@@ -19359,6 +20349,13 @@ Use SearchInput for text queries that filter or search a known content set.
|
|
|
19359
20349
|
|
|
19360
20350
|
`input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
|
|
19361
20351
|
|
|
20352
|
+
## Motion
|
|
20353
|
+
|
|
20354
|
+
- **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.
|
|
20355
|
+
- **response** · Input · `motion-interactive` — The focus ring and invalid border ease in, so focus reads as the field answering rather than a repaint.
|
|
20356
|
+
- **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.
|
|
20357
|
+
- **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.
|
|
20358
|
+
|
|
19362
20359
|
## Accessibility
|
|
19363
20360
|
|
|
19364
20361
|
- Renders a native <input type="search"> — keyboard and form semantics come for free.
|
|
@@ -20141,6 +21138,15 @@ Use ShowMore to reveal additional content after an intentionally shortened previ
|
|
|
20141
21138
|
|
|
20142
21139
|
`background`, `foreground`, `ring`
|
|
20143
21140
|
|
|
21141
|
+
## Motion
|
|
21142
|
+
|
|
21143
|
+
- **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.
|
|
21144
|
+
- **response** · Toggle · `motion-interactive` — The toggle is a link Button, so its hover and focus ease on Button's recipe.
|
|
21145
|
+
- **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.
|
|
21146
|
+
- **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.
|
|
21147
|
+
- **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.
|
|
21148
|
+
- **activity** · Root · _no motion_ — Expanding is local and instant; nothing loads.
|
|
21149
|
+
|
|
20144
21150
|
## Accessibility
|
|
20145
21151
|
|
|
20146
21152
|
- The toggle is a real <button> with aria-expanded reflecting the open state.
|
|
@@ -20214,6 +21220,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
|
|
|
20214
21220
|
| icons | boolean | true | Render the leading icon on each SidebarItem. Set false for a text-only rail. Ignored while collapsed (icon-only by definition). |
|
|
20215
21221
|
| 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. |
|
|
20216
21222
|
| 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. |
|
|
21223
|
+
| 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. |
|
|
20217
21224
|
|
|
20218
21225
|
## Variants
|
|
20219
21226
|
|
|
@@ -20223,6 +21230,14 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
|
|
|
20223
21230
|
|
|
20224
21231
|
`sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
|
|
20225
21232
|
|
|
21233
|
+
## Motion
|
|
21234
|
+
|
|
21235
|
+
- **arrival** · Root · _no motion_ — The rail is the app's frame, present from first paint. It is not summoned, so it has no entrance.
|
|
21236
|
+
- **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.
|
|
21237
|
+
- **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.
|
|
21238
|
+
- **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.
|
|
21239
|
+
- **activity** · Root · _no motion_ — Navigation chrome has no working state; a loading route shows it in the page.
|
|
21240
|
+
|
|
20226
21241
|
## Accessibility
|
|
20227
21242
|
|
|
20228
21243
|
- SidebarNav renders a <nav> landmark.
|
|
@@ -20308,11 +21323,19 @@ Use SideNav for vertical navigation inside a settings page, documentation area,
|
|
|
20308
21323
|
| --- | --- | --- | --- |
|
|
20309
21324
|
| 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. |
|
|
20310
21325
|
| 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. |
|
|
21326
|
+
| 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. |
|
|
20311
21327
|
|
|
20312
21328
|
## Tokens used
|
|
20313
21329
|
|
|
20314
21330
|
`muted`, `muted-foreground`, `foreground`, `accent`, `accent-foreground`, `ring`, `sidebar-foreground`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
|
|
20315
21331
|
|
|
21332
|
+
## Motion
|
|
21333
|
+
|
|
21334
|
+
- **arrival** · Root · _no motion_ — In-page navigation arrives with the page it navigates.
|
|
21335
|
+
- **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.
|
|
21336
|
+
- **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.
|
|
21337
|
+
- **activity** · Root · _no motion_ — Navigation chrome has no working state; a loading section shows it in the content.
|
|
21338
|
+
|
|
20316
21339
|
## Accessibility
|
|
20317
21340
|
|
|
20318
21341
|
- SideNav renders a <nav> landmark; pair multiple navs with aria-label to distinguish them.
|
|
@@ -20501,6 +21524,13 @@ Use Slider for bounded numeric choices where approximate adjustment is faster th
|
|
|
20501
21524
|
|
|
20502
21525
|
`secondary`, `primary`, `background`, `ring`
|
|
20503
21526
|
|
|
21527
|
+
## Motion
|
|
21528
|
+
|
|
21529
|
+
- **arrival** · Root · _no motion_ — It arrives with the form or panel it sits in.
|
|
21530
|
+
- **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.
|
|
21531
|
+
- **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.
|
|
21532
|
+
- **activity** · Root · _no motion_ — A slider commits as it moves; there is nothing to wait for.
|
|
21533
|
+
|
|
20504
21534
|
## Accessibility
|
|
20505
21535
|
|
|
20506
21536
|
- Base UI provides role=slider, aria-valuemin/max/now, and arrow-key / Page Up-Down / Home-End control.
|
|
@@ -20596,7 +21626,8 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
|
|
|
20596
21626
|
| variant | "line" \| "area" \| "bar" | "line" | A line, a line over a soft fill, or columns. |
|
|
20597
21627
|
| tone | "1" \| "2" \| "3" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | "primary" | Colour by role. |
|
|
20598
21628
|
| className | string | "h-8 w-24" | Sizes it; the drawing stretches to fit. |
|
|
20599
|
-
|
|
|
21629
|
+
| 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. |
|
|
21630
|
+
| animate | boolean | true | Draw on (or rise) when it scrolls into view. `animate={false}` is `motion="subtle"`. |
|
|
20600
21631
|
| locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
|
|
20601
21632
|
|
|
20602
21633
|
## Variants
|
|
@@ -20609,7 +21640,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
|
|
|
20609
21640
|
|
|
20610
21641
|
## Motion
|
|
20611
21642
|
|
|
20612
|
-
- **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.
|
|
21643
|
+
- **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.
|
|
20613
21644
|
- **response** · Sparkline · _no motion_ — Not interactive: it has no tooltip or focus.
|
|
20614
21645
|
- **transition** · Sparkline · _no motion_ — New data redraws in place; a word-sized chart animating on every update would distract from the number beside it.
|
|
20615
21646
|
- **activity** · Sparkline · _no motion_ — No indeterminate state.
|
|
@@ -21268,6 +22299,13 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
|
|
|
21268
22299
|
|
|
21269
22300
|
`border`, `muted`, `muted-foreground`
|
|
21270
22301
|
|
|
22302
|
+
## Motion
|
|
22303
|
+
|
|
22304
|
+
- **arrival** · Root · _no motion_ — A table renders with the page it sits in. Staggering rows in delays reading data someone opened to scan.
|
|
22305
|
+
- **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.
|
|
22306
|
+
- **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.
|
|
22307
|
+
- **activity** · Root · _no motion_ — Table is markup. Loading rows are Skeletons, which carry the shimmer.
|
|
22308
|
+
|
|
21271
22309
|
## Accessibility
|
|
21272
22310
|
|
|
21273
22311
|
- Renders semantic native table elements (thead/tbody/tfoot/th/td/caption) preserving built-in table semantics.
|
|
@@ -21335,7 +22373,7 @@ import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, Table
|
|
|
21335
22373
|
|
|
21336
22374
|
**Status:** stable
|
|
21337
22375
|
|
|
21338
|
-
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.
|
|
22376
|
+
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.
|
|
21339
22377
|
|
|
21340
22378
|
**Keywords:** tabs, tablist, panels, segmented content
|
|
21341
22379
|
|
|
@@ -21364,7 +22402,7 @@ Use Tabs to switch between related panels within the same page context.
|
|
|
21364
22402
|
|
|
21365
22403
|
| Name | Type | Description |
|
|
21366
22404
|
| --- | --- | --- |
|
|
21367
|
-
| 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. |
|
|
22405
|
+
| 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. |
|
|
21368
22406
|
| TabsTrigger | Base UI Tabs.Tab props (value, disabled) + className | A single tab button. value pairs it with the panel of the same value. |
|
|
21369
22407
|
| TabsPanel | Base UI Tabs.Panel props (value, keepMounted) + className | The content shown when the matching trigger is active. |
|
|
21370
22408
|
| TabsContent | Alias of TabsPanel | Re-export of TabsPanel for consumers that prefer this name. |
|
|
@@ -21374,17 +22412,27 @@ Use Tabs to switch between related panels within the same page context.
|
|
|
21374
22412
|
| Name | Type | Default | Description |
|
|
21375
22413
|
| --- | --- | --- | --- |
|
|
21376
22414
|
| Tabs (Root) | Base UI Tabs.Root props (value, defaultValue, onValueChange, orientation) | defaultValue: 0 | Controls the active tab. Uncontrolled via defaultValue; controlled via value + onValueChange. |
|
|
22415
|
+
| 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. |
|
|
21377
22416
|
|
|
21378
22417
|
## Tokens used
|
|
21379
22418
|
|
|
21380
22419
|
`border`, `muted`, `muted-foreground`, `foreground`, `ring`
|
|
21381
22420
|
|
|
22421
|
+
## Motion
|
|
22422
|
+
|
|
22423
|
+
- **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.
|
|
22424
|
+
- **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.
|
|
22425
|
+
- **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.
|
|
22426
|
+
- **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.
|
|
22427
|
+
- **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.
|
|
22428
|
+
|
|
21382
22429
|
## Accessibility
|
|
21383
22430
|
|
|
21384
22431
|
- Base UI provides the tablist/tab/tabpanel roles, aria-selected, and roving tabindex.
|
|
21385
22432
|
- Arrow keys move between tabs; Home/End jump to first/last; panels are linked via aria-controls.
|
|
21386
22433
|
- Active tab styling reads from the data-active attribute; disabled tabs from data-disabled. Disabled tabs are skipped by arrow-key navigation.
|
|
21387
22434
|
- Keyboard focus shows a ring on the focused trigger in both variants; the panel remains focusable.
|
|
22435
|
+
- The gliding indicator is role="presentation" decoration; aria-selected on the tab carries the state.
|
|
21388
22436
|
|
|
21389
22437
|
## Related
|
|
21390
22438
|
|
|
@@ -21416,6 +22464,14 @@ import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
|
|
|
21416
22464
|
<Tabs defaultValue="overview"><TabsList><TabsTrigger value="overview">Overview</TabsTrigger><TabsTrigger value="billing" disabled>Billing</TabsTrigger></TabsList><TabsPanel value="overview">Overview content</TabsPanel></Tabs>
|
|
21417
22465
|
```
|
|
21418
22466
|
|
|
22467
|
+
### Subtle motion
|
|
22468
|
+
|
|
22469
|
+
```tsx
|
|
22470
|
+
import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
|
|
22471
|
+
|
|
22472
|
+
<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>
|
|
22473
|
+
```
|
|
22474
|
+
|
|
21419
22475
|
---
|
|
21420
22476
|
|
|
21421
22477
|
# Textarea
|
|
@@ -21560,6 +22616,14 @@ Use ThemeSelector wherever a person picks a theme. The default `cards` layout is
|
|
|
21560
22616
|
|
|
21561
22617
|
`input`, `card`, `border`, `background`, `primary`, `primary-foreground`, `accent`, `muted`, `muted-foreground`, `ring`
|
|
21562
22618
|
|
|
22619
|
+
## Motion
|
|
22620
|
+
|
|
22621
|
+
- **arrival** · Root · _no motion_ — It sits in a settings panel or a popover, which owns the entrance.
|
|
22622
|
+
- **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.
|
|
22623
|
+
- **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.
|
|
22624
|
+
- **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.
|
|
22625
|
+
- **activity** · Root · _no motion_ — Applying a theme is synchronous: the whole interface repaints at once, and that repaint is the feedback.
|
|
22626
|
+
|
|
21563
22627
|
## Accessibility
|
|
21564
22628
|
|
|
21565
22629
|
- The container is role="radiogroup" with an aria-label; each card is a role="radio" button with aria-checked reflecting the selected theme.
|
|
@@ -21674,12 +22738,21 @@ Use ThemeSwitcher for light, dark, and system color mode preference.
|
|
|
21674
22738
|
| value | "light" \| "dark" \| "system" | — | The currently selected color preference. |
|
|
21675
22739
|
| onValueChange | (value: ThemeMode) => void | — | Called with the next color preference when an option is chosen. |
|
|
21676
22740
|
| label | string | "Theme" | Accessible label for the radiogroup. |
|
|
22741
|
+
| 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. |
|
|
21677
22742
|
| className | string | — | Merged onto the outer radiogroup container. |
|
|
21678
22743
|
|
|
21679
22744
|
## Tokens used
|
|
21680
22745
|
|
|
21681
22746
|
`border`, `card`, `muted-foreground`, `foreground`, `accent`, `accent-foreground`, `ring`
|
|
21682
22747
|
|
|
22748
|
+
## Motion
|
|
22749
|
+
|
|
22750
|
+
- **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.
|
|
22751
|
+
- **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.
|
|
22752
|
+
- **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.
|
|
22753
|
+
- **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.
|
|
22754
|
+
- **activity** · Root · _no motion_ — Changing the theme is synchronous; the repaint is the feedback.
|
|
22755
|
+
|
|
21683
22756
|
## Accessibility
|
|
21684
22757
|
|
|
21685
22758
|
- The container is role="radiogroup" with an aria-label; each option is role="radio" with aria-checked reflecting the selected value.
|
|
@@ -21719,6 +22792,14 @@ import { ThemeSwitcher } from "@sableui/react"
|
|
|
21719
22792
|
<ThemeSwitcher value="light" onValueChange={setTheme} />
|
|
21720
22793
|
```
|
|
21721
22794
|
|
|
22795
|
+
### Subtle motion
|
|
22796
|
+
|
|
22797
|
+
```tsx
|
|
22798
|
+
import { ThemeSwitcher } from "@sableui/react"
|
|
22799
|
+
|
|
22800
|
+
<ThemeSwitcher value={theme} onValueChange={setTheme} motion="subtle" />
|
|
22801
|
+
```
|
|
22802
|
+
|
|
21722
22803
|
---
|
|
21723
22804
|
|
|
21724
22805
|
# ThinkingOrb
|
|
@@ -21870,7 +22951,7 @@ Use Toast for transient feedback caused by a user action: saved, failed, copied,
|
|
|
21870
22951
|
| closeIcon | ReactNode | <Icon name="xmark" /> | Override the dismiss-button glyph. The default already follows the injected icon set. |
|
|
21871
22952
|
| useToast / useToastManager | () => { toasts, add, close, update, promise } | — | Manager hook for use inside components. add({ title, description, timeout?, type?, priority?, actionProps? }) returns the new toast id and applies the same persistence default as toast.* — an error, loading, or actionProps toast stays until dismissed unless you pass a timeout. close(id), update(id, opts), and promise(p, opts) round out the API. |
|
|
21872
22953
|
| toast | (message, options?) => string & { success, error, warning, info, message, loading, promise, dismiss, update, add, custom } | — | Imperative singleton callable OUTSIDE React (event handlers, utilities, non-React code) — the Sonner-shaped surface that makes migrating a lib/toast wrapper a near drop-in. Requires a mounted <ToastProvider> (it binds the shared manager). Each call returns the toast id; toast.dismiss(id?) closes one or all; toast.promise(p, { loading, success, error }) and toast.update(id, opts) handle async + in-place updates. |
|
|
21873
|
-
| type (toast option) | "success" \| "error" \| "warning" \| "info" \| "loading" | — | Set by the toast.* helpers. Drives a
|
|
22954
|
+
| type (toast option) | "success" \| "error" \| "warning" \| "info" \| "loading" | — | Set by the toast.* helpers. Drives a leading status glyph from the shared tone list, the same one Note and InlineStatus use (success→success, error→destructive, warning→warning, info→info), which blooms in as the toast lands; loading shows a spinner, and the glyph blooms in its place when the toast settles. Both loading and error persist until updated or dismissed unless the call supplies a timeout. |
|
|
21874
22955
|
|
|
21875
22956
|
## Tokens used
|
|
21876
22957
|
|
|
@@ -21880,6 +22961,7 @@ Use Toast for transient feedback caused by a user action: saved, failed, copied,
|
|
|
21880
22961
|
|
|
21881
22962
|
- **arrival** · ToastRoot · `motion-slide-left` — A toast interrupts without being asked, so where it came from is the reader's first question. Sliding in from the viewport edge answers it and marks the toast as belonging to the edge rather than to whatever it happens to cover.
|
|
21882
22963
|
- **response** · ToastAction · `motion-interactive` — Toasts are on a timer, so the action has to look actionable at a glance — there is no time to hunt for the hit target.
|
|
22964
|
+
- **response** · Status glyph · `motion-bloom` — The glyph is what says how it went, so it lands as its own beat: it blooms in 120ms after the card, and blooms again in the spinner's place when a loading toast settles. A 16px glyph needs that amplitude to read as an answer; the card itself only slides.
|
|
21883
22965
|
- **transition** · ToastRoot · `motion-slide-left` — Leaving the same way it entered keeps the stack legible when several dismiss at once; toasts that fade in place leave the remaining ones jumping to fill gaps with no visible cause.
|
|
21884
22966
|
- **transition** · ToastRoot (stacked) · _custom (outside the recipe layer)_ — A stacked toast arrives from the anchored edge in front of the others, which step back and shrink to make room; hovering fans the deck out into the list. Moving every card, rather than snapping them, shows that the new one went on top and the old ones are still there.
|
|
21885
22967
|
- **activity** · ToastRoot · _no motion_ — A toast reports a result that has already happened. Anything still in flight belongs in the surface that started it, where the reader is still looking.
|
|
@@ -21991,15 +23073,16 @@ Use Toggle for button-like controls that can be pressed on or off within a toolb
|
|
|
21991
23073
|
|
|
21992
23074
|
| Name | Type | Description |
|
|
21993
23075
|
| --- | --- | --- |
|
|
21994
|
-
| 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. |
|
|
23076
|
+
| 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. |
|
|
21995
23077
|
| ToggleGroupItem | Alias of Toggle | Re-export of Toggle for group composition — a Toggle carrying a value is the group item. |
|
|
21996
23078
|
|
|
21997
23079
|
## Props
|
|
21998
23080
|
|
|
21999
23081
|
| Name | Type | Default | Description |
|
|
22000
23082
|
| --- | --- | --- | --- |
|
|
22001
|
-
| 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. |
|
|
23083
|
+
| 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. |
|
|
22002
23084
|
| multiple | boolean | false | On ToggleGroup: allow several toggles pressed at once. The default is single-select, which behaves like a radio group. |
|
|
23085
|
+
| 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. |
|
|
22003
23086
|
| 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. |
|
|
22004
23087
|
|
|
22005
23088
|
## Variants
|
|
@@ -22010,6 +23093,14 @@ Use Toggle for button-like controls that can be pressed on or off within a toolb
|
|
|
22010
23093
|
|
|
22011
23094
|
`muted-foreground`, `foreground`, `accent`, `accent-foreground`, `card`, `border`, `ring`
|
|
22012
23095
|
|
|
23096
|
+
## Motion
|
|
23097
|
+
|
|
23098
|
+
- **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.
|
|
23099
|
+
- **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.
|
|
23100
|
+
- **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.
|
|
23101
|
+
- **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.
|
|
23102
|
+
- **activity** · Toggle · _no motion_ — A toggle commits instantly. Work that follows from the new state shows where its result appears, not on the control.
|
|
23103
|
+
|
|
22013
23104
|
## Accessibility
|
|
22014
23105
|
|
|
22015
23106
|
- Base UI sets aria-pressed on each Toggle and manages the pressed/disabled states.
|
|
@@ -22047,6 +23138,14 @@ import { Icon, Toggle, ToggleGroup } from "@sableui/react"
|
|
|
22047
23138
|
<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>
|
|
22048
23139
|
```
|
|
22049
23140
|
|
|
23141
|
+
### Segmented with labels, subtle motion
|
|
23142
|
+
|
|
23143
|
+
```tsx
|
|
23144
|
+
import { Toggle, ToggleGroup } from "@sableui/react"
|
|
23145
|
+
|
|
23146
|
+
<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>
|
|
23147
|
+
```
|
|
23148
|
+
|
|
22050
23149
|
### Multi-select group
|
|
22051
23150
|
|
|
22052
23151
|
```tsx
|
|
@@ -22228,6 +23327,124 @@ import { Icon, Toolbar, ToolbarButton, WorkspaceBar } from "@sableui/react"
|
|
|
22228
23327
|
|
|
22229
23328
|
---
|
|
22230
23329
|
|
|
23330
|
+
# ToolCall
|
|
23331
|
+
|
|
23332
|
+
**Status:** stable
|
|
23333
|
+
|
|
23334
|
+
One tool an agent called, as a quiet row inside its turn: a status glyph, what it did, the tool's name and where it is — with the arguments and result behind a disclosure.
|
|
23335
|
+
|
|
23336
|
+
**Keywords:** tool call, function call, tool use, agent step, mcp, ai, tool result, agent activity
|
|
23337
|
+
|
|
23338
|
+
## Usage
|
|
23339
|
+
|
|
23340
|
+
Use it for each tool call in an assistant's turn, so the person can see what the agent did and check any step. The row is the summary — glyph, a plain-words title, the tool's name, the status — and stays one line, so a turn with ten calls is still scannable. Input and output sit behind the disclosure, because they are read rarely and, when they are, in full.
|
|
23341
|
+
|
|
23342
|
+
### Best practices
|
|
23343
|
+
|
|
23344
|
+
- Do: Give it a `title` in plain words, in the tense of the status: "Searching the docs" while running, "Searched the docs" once done. The `name` stays beside it in mono for the person who wants to know exactly which tool ran.
|
|
23345
|
+
- Do: Drive `status` from the call's real state and keep the same element mounted as it changes. The spinner swaps for the outcome in place, and the change is announced to screen readers once.
|
|
23346
|
+
- Do: Pass `error` with `status="error"`. It shows under the row without opening anything, because a failure is the one result everyone needs to read.
|
|
23347
|
+
- Do: Pass a rendered result as `children` when the output is something to look at — a list of hits, a diff — rather than data to inspect. It replaces the output's code view, and `input` still shows above it.
|
|
23348
|
+
- Don't: Do not put the approve and deny buttons in a ToolCall. A call held for permission is `status="awaiting"` here, and the decision itself is ApprovalBlock, which says what will happen and asks once.
|
|
23349
|
+
- Don't: Do not use it for the agent's thinking or a progress note. A wait with no tool behind it is ThinkingOrb or a Spinner with a status line, and a row that claims a tool ran when none did makes every other row less trustworthy. → use **ThinkingOrb**, **Spinner**
|
|
23350
|
+
- Don't: Do not open every call by default. A turn whose calls all show their JSON buries the answer; leave `defaultOpen` for the one call the person is likely to check, if any.
|
|
23351
|
+
|
|
23352
|
+
### Anatomy
|
|
23353
|
+
|
|
23354
|
+
| Part | Required | Description |
|
|
23355
|
+
| --- | --- | --- |
|
|
23356
|
+
| Status glyph | yes | Spinner while running, then the outcome: check, warning, clock (awaiting approval) or ×. The only part that moves. |
|
|
23357
|
+
| Title | no | What the call does, in plain words. Without one, the tool's name leads. |
|
|
23358
|
+
| Name | yes | The tool's own name in mono, after the title. Hidden below the sm breakpoint when a title is present. |
|
|
23359
|
+
| Status | yes | One word for where the call is: Running, Needs approval, Done, Failed, Denied. |
|
|
23360
|
+
| Error line | no | The failure message, under the row and always visible. |
|
|
23361
|
+
| Details | no | Input and output as code, or a rendered result. A disclosure; the row is its trigger. |
|
|
23362
|
+
|
|
23363
|
+
## Props
|
|
23364
|
+
|
|
23365
|
+
| Name | Type | Default | Description |
|
|
23366
|
+
| --- | --- | --- | --- |
|
|
23367
|
+
| name | string | — | The tool's own name, as the model called it (`search_docs`). Shown in mono. |
|
|
23368
|
+
| title | ReactNode | — | What the call does, in words a person reads. Leads the row; omit it and `name` leads. |
|
|
23369
|
+
| status | "running" \| "awaiting" \| "complete" \| "error" \| "denied" | "running" | Where the call is. `awaiting` is held for the person's approval; `denied` is one they declined. |
|
|
23370
|
+
| statusLabel | string | — | Overrides the status word, for another language or a more specific phrase. |
|
|
23371
|
+
| input | unknown | — | The arguments. A string shows as is; anything else as formatted JSON. |
|
|
23372
|
+
| output | unknown | — | The result, shown the same way as `input`. |
|
|
23373
|
+
| error | ReactNode | — | What went wrong, for `status="error"`. Always visible under the row. |
|
|
23374
|
+
| children | ReactNode | — | A rendered result in place of `output`'s code view. |
|
|
23375
|
+
| defaultOpen | boolean | false | Open the details on mount. |
|
|
23376
|
+
| open | boolean | — | Controlled open state of the details. |
|
|
23377
|
+
| onOpenChange | (open: boolean) => void | — | Called when the details open or close. |
|
|
23378
|
+
|
|
23379
|
+
## Variants
|
|
23380
|
+
|
|
23381
|
+
- **status**: running, awaiting, complete, error, denied
|
|
23382
|
+
|
|
23383
|
+
## Tokens used
|
|
23384
|
+
|
|
23385
|
+
`card`, `border`, `destructive`, `muted`, `muted-foreground`, `foreground`, `tone-warning`, `ring`
|
|
23386
|
+
|
|
23387
|
+
## Motion
|
|
23388
|
+
|
|
23389
|
+
- **arrival** · Root · _no motion_ — It arrives as part of the assistant's turn, and the turn's own arrival covers it. A second entrance per call would make a busy turn twitch.
|
|
23390
|
+
- **response** · Header · `motion-interactive` — The header is the disclosure trigger but reads as a status row. The hover step is what tells the person it opens.
|
|
23391
|
+
- **transition** · Status glyph · `motion-icon-swap` — The spinner becoming a check or a warning is the moment the call settles, and it is the one change in the row worth catching from the corner of the eye. The swap reads as one mark replaced, not a flicker.
|
|
23392
|
+
- **transition** · Details · `motion-disclosure` — The height change connects the code that appears to the row that was pressed, in a thread where several rows look alike.
|
|
23393
|
+
- **activity** · Status glyph · `motion-spin` — While the call runs, the spinner is what separates working from stuck. It pauses once the call settles, so a long thread of finished calls costs nothing.
|
|
23394
|
+
|
|
23395
|
+
## Accessibility
|
|
23396
|
+
|
|
23397
|
+
- With details, the header is a button with aria-expanded (Base UI Collapsible); its name is the title, the tool name and the status word, so it reads as one sentence.
|
|
23398
|
+
- A status change after mount is announced once through a polite role=status region ("Searched the docs: Done"). The status a call mounts with is not announced, so a thread restored from history stays quiet.
|
|
23399
|
+
- The glyph is aria-hidden: the status word beside it carries the meaning, so colour and shape are never the only signal.
|
|
23400
|
+
|
|
23401
|
+
## Related
|
|
23402
|
+
|
|
23403
|
+
- Components: Message, ThinkingOrb, StreamingText, CodeBlock, Collapsible, InlineStatus
|
|
23404
|
+
|
|
23405
|
+
## Examples
|
|
23406
|
+
|
|
23407
|
+
### A finished call
|
|
23408
|
+
|
|
23409
|
+
```tsx
|
|
23410
|
+
import { ToolCall } from "@sableui/react"
|
|
23411
|
+
|
|
23412
|
+
<ToolCall
|
|
23413
|
+
name="search_docs"
|
|
23414
|
+
title="Searched the docs"
|
|
23415
|
+
status="complete"
|
|
23416
|
+
input={{ query: "date range picker" }}
|
|
23417
|
+
output={{ hits: ["DatePicker", "Calendar", "DateTile"] }}
|
|
23418
|
+
/>
|
|
23419
|
+
```
|
|
23420
|
+
|
|
23421
|
+
### Running, then failed
|
|
23422
|
+
|
|
23423
|
+
```tsx
|
|
23424
|
+
import { ToolCall } from "@sableui/react"
|
|
23425
|
+
|
|
23426
|
+
<div className="flex flex-col gap-2">
|
|
23427
|
+
<ToolCall name="read_file" title="Reading package.json" status="running" input={{ path: "package.json" }} />
|
|
23428
|
+
<ToolCall
|
|
23429
|
+
name="run_tests"
|
|
23430
|
+
title="Ran the tests"
|
|
23431
|
+
status="error"
|
|
23432
|
+
error="3 of 412 tests failed in date-picker.test.tsx."
|
|
23433
|
+
input={{ filter: "date-picker" }}
|
|
23434
|
+
/>
|
|
23435
|
+
</div>
|
|
23436
|
+
```
|
|
23437
|
+
|
|
23438
|
+
### Waiting for approval
|
|
23439
|
+
|
|
23440
|
+
```tsx
|
|
23441
|
+
import { ToolCall } from "@sableui/react"
|
|
23442
|
+
|
|
23443
|
+
<ToolCall name="send_email" title="Send the invoice to Ada" status="awaiting" input={{ to: "ada@example.com", subject: "INV-1042" }} />
|
|
23444
|
+
```
|
|
23445
|
+
|
|
23446
|
+
---
|
|
23447
|
+
|
|
22231
23448
|
# Tooltip
|
|
22232
23449
|
|
|
22233
23450
|
**Status:** stable
|
|
@@ -22265,7 +23482,8 @@ Use Tooltip for brief nonessential hints tied to a specific control or piece of
|
|
|
22265
23482
|
| TooltipTrigger | Base UI Trigger props + render | The anchor element. Use render={<Button/>} to compose with another component. |
|
|
22266
23483
|
| TooltipContent | Base UI Popup props + side \| align \| sideOffset + className | Portals + positions + paints the popup. side ("top" \| "bottom" \| "left" \| "right" \| "inline-*") picks which side of the trigger to render against, defaults to "top", and auto-flips on collision; align ("start" \| "center" \| "end") defaults to "center"; sideOffset (gap between trigger and popup, in pixels) defaults to 6. |
|
|
22267
23484
|
| TooltipArrow | Base UI Arrow props + className | Optional pointer connecting the popup to its trigger — a popover-coloured square turned 45° that carries the border on its outward edges. Place inside TooltipContent. |
|
|
22268
|
-
| SimpleTooltip | Base UI Tooltip.Root props + content: ReactNode + children: ReactElement + side \| align \| sideOffset | Shorthand wrapping Root + Trigger + Content for the common case. The single child becomes the trigger (via Base UI render); content sets the body; side / align / sideOffset forward to the content with the same defaults. Composition parts remain for advanced use. |
|
|
23485
|
+
| SimpleTooltip | Base UI Tooltip.Root props + content: ReactNode + children: ReactElement + side \| align \| sideOffset | Shorthand wrapping Root + Trigger + Content for the common case. The single child becomes the trigger (via Base UI render); content sets the body; side / align / sideOffset forward to the content with the same defaults. Composition parts remain for advanced use. Inside a TooltipGroup it becomes a trigger of the group's shared tooltip instead. |
|
|
23486
|
+
| TooltipGroup | { children: ReactNode; motion?: "full" \| "subtle" \| "off" } | One tooltip shared by every SimpleTooltip inside it — for a toolbar or a row of icon buttons. Moving the pointer along the row glides the tooltip to the next trigger and swaps its label (old fades out, new fades in, each sliding toward the direction of travel), instead of closing one tooltip and opening another a few pixels away. Renders no element. Each SimpleTooltip keeps its own content / side / align / sideOffset and `disabled`; root props such as open / onOpenChange belong to the shared tooltip and are ignored inside a group. `motion` turns it down: `subtle` jumps between triggers and only cross-fades the label. The composable Tooltip parts are unaffected. |
|
|
22269
23487
|
|
|
22270
23488
|
## Props
|
|
22271
23489
|
|
|
@@ -22283,6 +23501,7 @@ Use Tooltip for brief nonessential hints tied to a specific control or piece of
|
|
|
22283
23501
|
- **arrival** · Content · `motion-pop` — The transform-origin follows the anchor, so the tooltip appears to grow out of the element it describes. That is what binds it to its trigger — a bubble that materialises at full size near two possible triggers belongs to neither.
|
|
22284
23502
|
- **response** · Content · _no motion_ — The tooltip is not a hover target; pointing at it is how you dismiss it. Styling it for the pointer would suggest otherwise.
|
|
22285
23503
|
- **transition** · Content · `motion-pop` — Same recipe, exit direction — it collapses back toward the anchor. Content that fades from a fixed size on close reads as a second, unrelated element disappearing.
|
|
23504
|
+
- **transition** · TooltipGroup · _custom (outside the recipe layer)_ — Moving between triggers in a group, the one tooltip glides to the next trigger, resizes to its label and cross-fades the text with a short slide toward the direction of travel. Separate tooltips would close here and open there, two surfaces blinking side by side; the glide says it is the same hint following the pointer. Base UI's handle + Viewport supply the shared popup and the previous/current content; the timing is the expressive glide (slow, emphasized), so `subtle` keeps only the cross-fade.
|
|
22286
23505
|
- **activity** · Content · _no motion_ — Tooltip copy is resident before the tooltip opens. Anything that has to be fetched is a Popover with its own loading state.
|
|
22287
23506
|
|
|
22288
23507
|
## Accessibility
|
|
@@ -22322,6 +23541,14 @@ import { Button, Icon, SimpleTooltip } from "@sableui/react"
|
|
|
22322
23541
|
<SimpleTooltip content="Saved to your workspace."><Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button></SimpleTooltip>
|
|
22323
23542
|
```
|
|
22324
23543
|
|
|
23544
|
+
### One tooltip gliding along a toolbar (TooltipGroup)
|
|
23545
|
+
|
|
23546
|
+
```tsx
|
|
23547
|
+
import { Icon, SimpleTooltip, Toolbar, ToolbarButton, TooltipGroup } from "@sableui/react"
|
|
23548
|
+
|
|
23549
|
+
<TooltipGroup><Toolbar aria-label="Formatting"><SimpleTooltip content="Bold"><ToolbarButton size="icon-sm" aria-label="Bold"><Icon name="bold" /></ToolbarButton></SimpleTooltip><SimpleTooltip content="Italic"><ToolbarButton size="icon-sm" aria-label="Italic"><Icon name="italic" /></ToolbarButton></SimpleTooltip><SimpleTooltip content="Insert link"><ToolbarButton size="icon-sm" aria-label="Insert link"><Icon name="link" /></ToolbarButton></SimpleTooltip></Toolbar></TooltipGroup>
|
|
23550
|
+
```
|
|
23551
|
+
|
|
22325
23552
|
---
|
|
22326
23553
|
|
|
22327
23554
|
# Tree
|
|
@@ -22387,11 +23614,19 @@ Use Tree for the hierarchy panel in a workspace: layers, objects, an inspector o
|
|
|
22387
23614
|
| disclosureIcon | ReactNode | — | Replaces TreeRow's semantic chevron-down icon. Omit to use the chevron-down from the app's IconProvider set. |
|
|
22388
23615
|
| actions | ReactNode | — | Right-rail controls for a TreeRow: visibility, solo, a row menu. |
|
|
22389
23616
|
| label | ReactNode | — | TreeAddRow's visible label. Name what gets added — 'Add effect', not 'Add'. |
|
|
23617
|
+
| motion | "full" \| "subtle" \| "off" | "full" | On Tree. 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` expanded rows appear at once instead of unfolding; the chevron still turns. |
|
|
22390
23618
|
|
|
22391
23619
|
## Tokens used
|
|
22392
23620
|
|
|
22393
23621
|
`panel-raised`, `hairline`, `foreground`, `muted-foreground`, `primary`, `ring`
|
|
22394
23622
|
|
|
23623
|
+
## Motion
|
|
23624
|
+
|
|
23625
|
+
- **arrival** · TreeRow · `motion-arrive` — Rows a user's expand reveals fade down into place in document order (a capped 18ms stagger), so the eye can follow where the children came from. An animation (motion-arrive) rather than a transition, because the rows already wear motion-interactive and a second transition list would fight it. Only on a user expand of a `subtree` — a tree that mounts open shows its rows at once, and rows added later to an open folder do not cascade. Expressive: `subtle` shows them at once.
|
|
23626
|
+
- **response** · TreeRow · `motion-interactive` — Hover and selection fills ease, so sweeping the pointer down a dense panel reads as a highlight following it rather than rows flickering.
|
|
23627
|
+
- **transition** · Disclosure · `motion-interactive` — The chevron turns with the rows arriving under it; motion-interactive lists `rotate`, which is what -rotate-90 writes, so it no longer flips in one frame.
|
|
23628
|
+
- **activity** · Tree · _no motion_ — A tree has no indeterminate state of its own; a branch that loads its children should render a TreeEmpty or a loading row inside it.
|
|
23629
|
+
|
|
22395
23630
|
## Accessibility
|
|
22396
23631
|
|
|
22397
23632
|
- Tree implements the ARIA tree view pattern: Tree is role="tree", each TreeRow and TreeAddRow is role="treeitem" with aria-level, expandable rows carry aria-expanded, and a TreeSection is a role="group" labelled by its heading. Rows stay flat in the DOM — hierarchy is aria-level — so the fills span the panel's full width.
|
|
@@ -22586,6 +23821,14 @@ Use Video to embed playable or previewable video media inside a page.
|
|
|
22586
23821
|
|
|
22587
23822
|
`border`, `overlay`, `overlay-foreground`, `ring`
|
|
22588
23823
|
|
|
23824
|
+
## Motion
|
|
23825
|
+
|
|
23826
|
+
- **arrival** · Root · _no motion_ — The frame arrives with its page. Once there is a source, the poster and controls are the browser's.
|
|
23827
|
+
- **response** · Play button · `motion-interactive` — With `onRequestPlay`, the play disc firms its scrim on hover, eased, so the mark reads as pressable over any image.
|
|
23828
|
+
- **response** · Play button · `motion-press` — Loading a source can take a moment; the settle on press confirms the tap before the player appears.
|
|
23829
|
+
- **transition** · Root · _no motion_ — Placeholder to player is a swap of elements: the native video mounts where the poster was. Animating the swap would delay the playback the person just asked for.
|
|
23830
|
+
- **activity** · Root · _no motion_ — Buffering belongs to the native controls, which draw their own indicator. A second loader over the frame would compete with the browser's.
|
|
23831
|
+
|
|
22589
23832
|
## Accessibility
|
|
22590
23833
|
|
|
22591
23834
|
- Mounts a native <video> element, preserving built-in keyboard controls when controls is enabled.
|
|
@@ -23251,7 +24494,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
23251
24494
|
- **Instead of** An animated fade or a hand-written `mask-image` gradient. Use `StreamingText` first; this is for a stream rendered without it.
|
|
23252
24495
|
- `<span className="stream-tail">{latestChunk}</span>`
|
|
23253
24496
|
|
|
23254
|
-
### motion (
|
|
24497
|
+
### motion (31)
|
|
23255
24498
|
|
|
23256
24499
|
- **`motion-interactive`** — The transition for interactive chrome: colour, background, border, shadow, opacity, fill, stroke, scale and translate at the fast duration and standard easing. Also removes the mobile tap delay and the platform's grey tap flash. *Use for:* On anything clickable that changes on hover, focus, press or selection: a custom row, a tile, a link, an app-owned button. It is the `response` slot, and the only transition declarer such an element needs.
|
|
23257
24500
|
- **Instead of** Tailwind's colour-only or all-property transition utilities with a hand-picked duration. Those ignore --motion-scale, skip the transform properties `motion-press` needs, and leave the 300ms tap delay on touch screens. sable-lint's `static-interactive` fires on a hover style with no recipe.
|
|
@@ -23283,6 +24526,31 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
23283
24526
|
- **`motion-icon-swap`** — Cross-fades two glyphs stacked in one cell when a control's icon changes with its state: the outgoing one shrinks and blurs out, the incoming one resolves in. The inactive glyph is the one marked `aria-hidden="true"` (or `data-active="false"`). *Use for:* Play/pause, mute/unmute, outline/fill, copy/check, menu/close: one control whose icon changes. Keep both icons mounted; the correct accessibility markup is also the animation hook.
|
|
23284
24527
|
- **Instead of** Rendering one icon or the other (it snaps between two frames), or absolutely positioning the second icon, which collapses the wrapper. Under reduced motion the swap is instant and the glyph still changes.
|
|
23285
24528
|
- `<span className="motion-icon-swap"><Icon name="play" aria-hidden={playing} /><Icon name="pause" aria-hidden={!playing} /></span>`
|
|
24529
|
+
- **`motion-glide`** — Timing for ONE element that travels to the selected option — position and size ease on the emphasized curve at the slow step. Expressive: `data-motion="subtle"` makes it snap. *Use for:* A selection indicator you position yourself: a custom nav's current-page bar, a segmented control's thumb, an active dot. Tabs, ToggleGroup `segmented`, ThemeSwitcher and Carousel already glide.
|
|
24530
|
+
- **Instead of** A fill or border on each option that switches on and off (two things blink instead of one thing moving), or a hand-written transition with a literal duration.
|
|
24531
|
+
- `<span aria-hidden className="motion-glide absolute bottom-0 h-0.5 bg-foreground" style={{ left: x, width: w }} />`
|
|
24532
|
+
- **`motion-shake`** — A short decaying horizontal shake that plays while `data-shake` is set. `useShake()` sets and clears the attribute and restarts it on a repeat refusal. Expressive: under `subtle` only the error colour remains. *Use for:* An answer the system refused: a wrong one-time code, a submit with invalid fields, a rejected password. Always alongside the error colour and message, never instead of them. OtpField's `invalid` already shakes.
|
|
24533
|
+
- **Instead of** A keyframe shake with a literal duration, which keeps moving under reduced motion, or a shake with no error text.
|
|
24534
|
+
- `const { shake, shakeProps } = useShake()
|
|
24535
|
+
<form className="motion-shake" {...shakeProps} onSubmit={(e) => { if (!valid) { e.preventDefault(); shake() } }}>…</form>`
|
|
24536
|
+
- **`motion-tick`** — A selection mark that draws itself in from its leading edge (a clip-path wipe, so it works on any icon set) with an essential fade, keyed to Base UI's starting/ending style. Exits only fade. The wipe is expressive. *Use for:* A check or success glyph that mounts with its state: a custom selection card's tick, a completed step. Checkbox and CheckboxCard already use it.
|
|
24537
|
+
- **Instead of** A scale-from-zero pop on a check mark, which reads as a stamp rather than a tick, or a path-length draw that needs to own the SVG.
|
|
24538
|
+
- `<Base.Indicator className="motion-tick"><Icon name="check" /></Base.Indicator>`
|
|
24539
|
+
- **`motion-appear`** — Fades and scales a small control in on first render through `@starting-style` — no data attribute, no JS. The fade is essential, the scale is expressive. Removal is instant. *Use for:* A control that mounts in place as state changes: a search field's clear button, a send button replacing a mic, a count badge that appears. Put it on a wrapper around a Sable Button or anything carrying `motion-interactive`, so the two do not fight over `transition-property`.
|
|
24540
|
+
- **Instead of** Conditionally rendering a control so it pops in on one frame, or wrapping a single icon button in Presence.
|
|
24541
|
+
- `{value && <span className="flex motion-appear"><Button size="icon-xs" variant="ghost" aria-label="Clear"><Icon name="xmark" /></Button></span>}`
|
|
24542
|
+
- **`motion-arrive`** — Items a user action brings into a list settle into place one after another: a short fade down from 0.25rem. Stagger with `--motion-arrive-delay` (raw ms; the recipe scales it). Expressive. *Use for:* Only on items that are NEW because of something the user did: a folder's children on expand, a row just added, results that load in. Tree's expand already uses it.
|
|
24543
|
+
- **Instead of** A staggered entrance on every render of a list, which turns a list into a loading delay, or an inline keyframe animation with literal durations.
|
|
24544
|
+
- `<li className="motion-arrive" style={{ "--motion-arrive-delay": "36ms" } as CSSProperties}>{row.name}</li>`
|
|
24545
|
+
- **`motion-bloom`** — A small glyph landing in its "on" state grows from nothing past its size (1.2) and settles. The amplitude a 16px glyph needs to read as an event; surfaces never bloom. A keyframe that plays on mount, so remount the glyph (a changing `key`) to replay it. `--motion-bloom-delay` holds it at nothing first (raw ms; the recipe scales it). Expressive. *Use for:* A glyph that a person's choice turns on: a liked heart, a starred item, a status icon arriving in a toast. LikeButton and Toast already use it. Pair with <Burst> for the follow-through on a celebratory toggle.
|
|
24546
|
+
- **Instead of** A bigger motion-press or motion-pop on the whole control (a button or card that overshoots 20% lurches), or a hand-written scale keyframe with literal durations. Never on the resting state: a page that loads with a post already liked must not bloom every heart.
|
|
24547
|
+
- `<span key={likes} className={liked ? "motion-bloom" : undefined}><Icon name="heart" variant={liked ? "solid" : "regular"} /></span>`
|
|
24548
|
+
- **`motion-size`** — Eases an element between sizes: between a keyword and a length (0 ↔ auto) through `interpolate-size: allow-keywords`, and between measured pixel sizes when paired with `useAutoSize()`, so a button whose label changes stretches to the new width instead of jumping. A superset of motion-interactive's transition list, so it can sit on a Button. The size is expressive. *Use for:* A control whose content changes length in place: a two-step confirm button, a Copy → Copied label, a chip that gains a count. ConfirmButton already uses it.
|
|
24549
|
+
- **Instead of** A hand-written width transition, or a fixed width that clips the longer label. A content-sized box computes `width: auto` before and after its label changes, so on its own the recipe has nothing to ease — that is what `useAutoSize()` is for.
|
|
24550
|
+
- `const size = useAutoSize<HTMLButtonElement, HTMLSpanElement>()
|
|
24551
|
+
<button type="button" ref={size.outerRef} className="motion-size">
|
|
24552
|
+
<span ref={size.innerRef} className="inline-flex w-max">{copied ? "Copied" : "Copy link"}</span>
|
|
24553
|
+
</button>`
|
|
23286
24554
|
- **`motion-spin`** — A continuous rotation for a spinner glyph, scaled by --motion-scale so reduced motion freezes it. *Use for:* A loading glyph that is not the Spinner component (use Spinner first), such as an icon inside a busy button.
|
|
23287
24555
|
- **Instead of** Tailwind's built-in spin animation, which ignores --motion-scale. sable-lint's `builtin-animate` flags it.
|
|
23288
24556
|
- `<Icon name="spinner" className="motion-spin" />`
|