Releases: saasflare/saasflare-ui
Release list
@saasflare/ui@3.7.0
Minor Changes
-
4259743: Bundleless (per-module) dist — the structural fix behind the bundle-perf findings.
Every source file now compiles to its own dist module (490 files) instead of one bundled mega-entry per format. All public entry paths (
dist/index.*,dist/entries/*) are unchanged; consumer imports stay identical. What changes:- RSC granularity: the
"use client"directive is injected per module — 226 modules ship server-eligible (was 6 chunks), includingcn,PALETTES, and the type/constant modules. The main barrel itself carries no directive anymore.createSafeContextmoved fromlib/utilsinto its own client module so the pure utilities stay server-safe. - Tree-shaking: consumer bundlers prune per module. A
cn-only import dropped from ~131 KB to ~26 KB even withoutsideEffectshints, and to near-zero in bundlers that honor the package'ssideEffectsfield. - Peer confinement:
react-hook-formis referenced only byform.*andreact-day-pickeronly bycalendar.*— no longer baked into the shared entry chunk. (They remain required peers for now: CJSrequireof the barrel and Vite dep-prebundling still resolve the full graph.) - Declarations now come from
tsc(per-file, matching the module layout) instead of rollup-dts.
Validated: every relative specifier in dist resolves (1,420 checked), CJS barrel loads 455 exports, package + app typecheck clean, and a full Next production build of the catalog (141 pages) passes against the new dist.
- RSC granularity: the
-
6abccab: Hooks roster, sonner singleton fix, and full JSDoc coverage.
- 21 hooks added to the public barrel (previously unreachable):
useAnimationFrame,useClickOutside,useClipboard,useDebounce,useDebouncedCallback,useDocumentTitle,useEventListener,useIdle,useInView,useIntersectionObserver,useKeyboardShortcut,useLongPress,useMediaQuery,useMounted,useMousePosition,useOnline,useParallax,usePrevious,useScrollLock,useScrollPosition,useToggle,useWindowSize— with their option/return types. - Hook fixes applied before export:
useDebouncedCallback: generic constraint now accepts concretely-typed callbacks (its own @example previously failedtsc), andmaxWaitre-arms per burst instead of firing once per component lifetime.useIntersectionObserver: inline arraythreshold(the documented usage) no longer re-creates the observer in an infinite loop; observer callback reads the last entry of a batch.useEventListener: inlineoptionsobjects no longer tear the listener down every render (deps key on option primitives) — fixes brokenoncesemantics.useLongPress: pending long-press timer cleared on unmount; latest-ref writes moved out of the render body.useScrollLock: restores the body's originalpadding-rightinstead of clobbering it.useClipboard: reset timer cleared on unmount.
- sonner toast fix:
sonneris no longer inlined into the bundle (noExternalremoved) — the inlined copy had its own toast state, so consumers'toast()calls never rendered in the package's<Toaster>.toastis now also re-exported from@saasflare/uiso app code and the Toaster always share one sonner instance. - JSDoc on every export is now real: 218 previously undocumented exported declarations across 56 files got descriptions (the repo contract feeds react-docgen → docs site); generated docs now show descriptions for 300 of 303 components (previously dozens were blank).
- Removed the duplicate non-prologue
"use client"directive from 27 remaining files.
- 21 hooks added to the public barrel (previously unreachable):
-
eed071a: React-correctness fixes (adversarially verified, incl. one Playwright-confirmed critical) + honest peer declarations.
- Switch: the thumb never moved. Its only movement mechanism was a Motion
layoutprop, which is silently inert under thedomAnimationLazyMotion bundle SaasflareShell loads (layout projection lives indomMax) — and there was no layout change to animate anyway. Verified live: the thumb's bounding box was pixel-identical in both states; only the track color changed. Now a CSStranslate-xtransform sized perdata-size, honoringanimated={false}andprefers-reduced-motion. - MultiSelect: removed the equally-inert
layoutprop from chips (enter/exit animations unchanged and working). - useCountdown: no longer reads the clock during render (guaranteed React hydration mismatch on SSR). First paint renders zeros; the live value fills in after mount. An already-expired target no longer starts a timer.
- TagInput: pasting
"a, b, c"kept only the last tag (stale-closure commit loop). Multi-separator pastes now land in one batched state update. - Confetti / TypewriterText: an inline
onCompletecallback no longer restarts the animation on every parent render (latest-ref; effects keyed on real triggers only). - Peers made honest:
react-hook-formandreact-day-pickerare now required peers — the bundled dist imports them eagerly from the main entry, so a default install without them failed at build/require time while the README claimed they were optional. (Proper fix — per-module dist so they become truly optional again — tracked as a follow-up.) README updated; sonner section rewritten to match the un-inlining from the previous changeset.
- Switch: the thumb never moved. Its only movement mechanism was a Motion
-
6abccab: Strict-review fix batch — correctness, a11y, and contract repairs across the package.
Behavioral fix (flagged):
MultiSelect.closeOnSelectsemantics were inverted relative to the prop name (truekept the popover open). The prop now means what it says —truecloses on each pick — and the default flipped tofalse, so default behavior is unchanged. Only consumers who passedcloseOnSelectexplicitly see a change (they previously got the opposite of what the name promised).Fixes:
- Button
variant="shadow"rendered no shadow (consumed--btn-shadow, which was never defined). Now renders a palette-aware OKLCH intent shadow. - PALETTES/PaletteId extended from 20 to all 26 palettes defined in
palettes.css— addssaasflare(house palette),lavender,mint,sage,sky,snow. - useFileDialog no longer pins first-call
onChange/accept/multiple/capture/directoryforever (options re-applied on everyopen(), listener reads a latest-ref) and removes its hidden<input>on unmount. Fixes stale validation/callbacks in Dropzone's click path. - DataTable no longer sets
role="button"on clickable<tr>(it destroyed table semantics for screen readers); rows stay focusable with Enter/Space activation and exposedata-clickable. - MultiSelect chip-remove and clear-all affordances are no longer focusable controls nested inside the trigger
<button>(invalid interactive nesting); they are mouse-only now, with Backspace on the trigger/empty search input as the keyboard path (trigger Backspace added). - NotificationCenter rows use a stretched-overlay action instead of nesting the mark-as-read
<button>inside a row<button>/<a>; unread state is now announced (sr-only text +Unread:action label). - DatePicker/DateRangePicker controlled clear works: controlled-ness is latched (writing back the
undefinedemitted on deselect no longer flips the component to uncontrolled), andvalueacceptsnullfor controlled-empty. - Size axis unified on
"md": Avatar, Switch, NativeSelect, SelectTrigger, Toggle, ToggleGroup, Item, SidebarMenuButton, ThemeModeMultiToggle migrate"default"→"md"(canonicalSizescale)."default"remains accepted as a deprecated alias. - SocialButton deprecated in favor of
SocialAuthButton; its conflictingSocialProvidertype renamed toSocialButtonProvider(the package-levelSocialProviderexport is the 16-provider union); hardcoded grays/hex replaced with tokens. - SaasflareProvider no longer crashes on corrupted persisted prefs (
null/non-object localStorage values normalize to defaults, matching the inline script's defense). - snow palette pins a visible focus ring per mode; achromatic palette joins the fixed 5-hue chart override group (charts were collapsing to grayscale).
- Removed duplicate
"use client"directives (form, use-local-storage); fixed the stalethemes.cssreference in the styles entry docs.
- Button
Patch Changes
-
9f83c23: Runtime-perf cleanups (verified review minors) + full catalog coverage.
- SpotlightCard / GlowingEffect: cursor tracking moved out of React state — position is written as CSS variables directly on the overlay (no per-frame re-render), and the container rect is cached for the hover duration instead of a forced-layout
getBoundingClientRectper mousemove (invalidated on enter/scroll/resize). - MouseGradientBlob: same rect caching in the mousemove hot path.
- DockItem: item centers are cached instead of reading
getBoundingClientRectfor every item on every mouseX update while the width spring writes layout per frame (the classic read/write thrash interleave). - ScrollToTopButton: the scroll-container resolver gives up after ~10s with a dev warning instead of polling
getElementByIdevery frame forever when the id never mounts. - Docs:
LogoandAppIconjoin the catalog (registry, props, doc pages, demos) — the generated docs now cover all 130 public components.
- SpotlightCard / GlowingEffect: cursor tracking moved out of React state — position is written as CSS variables directly on the overlay (no per-frame re-render), and the container rect is cached for the hover duration instead of a forced-layout
-
bc0ca46: Test baseline + PR CI.
- vitest + testing-library regression suite (13 tests) pinning every bug class fixed in the review batches: Switch thumb movement, MultiSelect
closeOnSelectsemantics, DatePicker controlled clear (incl.null), TagInput separator paste,useDebouncedCallbackmaxWait re-arm + concrete-callback typing,useCountdownSSR-safe first paint + expired-target timer,useFileDialogoption re-application + unmount cleanup,useScrollLockpadding restore, and the 26-palette PALETTES lockstep. - GitHub Act...
- vitest + testing-library regression suite (13 tests) pinning every bug class fixed in the review batches: Switch thumb movement, MultiSelect
@saasflare/ui@3.6.0
Minor Changes
-
ec4a289: Consistency, a11y, and correctness fixes across the component set.
iconWeight no longer leaks to the DOM
- ~50 components extended
SaasflareComponentProps(which includesiconWeight) but only forwardedsurface/radius/animatedto the resolver, lettingiconWeightfall through...propsonto a DOM/Radix/motion element — emitting an unknowniconWeightattribute (React 19 warning) for anyone who set it. Every affected component now consumesiconWeightvia the resolver, so the axis is honored and never reaches the DOM.
Theming / API
AlertDialogContentandSheetContentnow honor thesurfaceaxis (usesurface-card, matchingDialog/Drawer) instead of a hardcodedbg-background— thesurfaceprop they already advertised now actually renders.Badgevariantnow accepts the deprecated aliases"default" | "destructive" | "secondary"in its type (previously a runtime-only compat map that TypeScript users could not reach).DockItemPropsno longer extendsSaasflareComponentProps— it advertisedsurface/radius/animated/iconWeightbut ignored them (aDockIteminherits motion from its parentDockvia context). Type-narrowing note: if you passed these no-op props toDockItem, remove them.
Correctness
Dockmagnification usedpageXagainst viewport-space bounds, so items magnified at the wrong position once the page scrolled horizontally — now usesclientX.SparkChartvariant="bar"computed negative bar widths past ~40 points (bars vanished); switched to a proportional slot layout that scales to any point count.Dropzonedrag-active state no longer flickers as the cursor crosses child elements (drag-depth counter instead of a naivedragleave).TimelineItemrenders a plain<div>when motion is disabled, so motion-off consumers don't instantiate a motion node per item or require theLazyMotionprovider just for a timeline.
Accessibility
FlipWordsannounces word changes — thearia-liveregion moved from the per-cycle keyed node (never announced) to the stable container.AnimatedTooltipavatars are now keyboard-focusable and reveal their tooltip on focus, with an accessible label.Ratingexposesaria-valuetext(e.g. "4.5 of 5 stars") so half-star values are announced with context.Dockitems reveal their tooltip on keyboard focus, not just hover.Stepperno longer setsaria-current="step"on two nested elements — the decorativeStepsindicator is hidden from assistive tech, leaving the interactive trigger buttons as the single source.
- ~50 components extended
@saasflare/ui@3.5.0
Minor Changes
-
5a691ea: Launch-readiness fixes: two broken cursor effects, modal a11y, focus rings, and two unreachable brand exports.
Critical — effects that never worked
GlowingEffect: the cursor tracking and hover state lived on thepointer-events-noneoverlay, so the listeners never fired and the glow stayed inert. Tracking now runs on the positioned parent the overlay covers (the same patternSpotlightCardalready uses); the overlay stayspointer-events-none.MouseGradientBlob: themousemovelistener was bound to the component's ownpointer-events-noneroot, so the blob never moved. It now binds to the positioned host element.
New public exports (previously documented but unreachable)
Logo/AppIcon(andLogoProps/AppIconProps) are now exported from the package root. Their JSDoc advertisedimport { Logo } from '@saasflare/ui', but they were never re-exported, so the documented import threw.
Accessibility
GalleryLightboxnow traps focus and locks body scroll via the shareduseFocusTrap/useScrollLockhooks (matchingHeroVideoDialog), with focus moved in on open and restored on close. Previously it was anaria-modaldialog with no focus management.Input/Textareanow render a motion-independent CSS focus ring (focus-visible:ring-[3px] ring-ring/50), matchingNativeSelect/SearchField/Select. The focus ring no longer disappears underprefers-reduced-motionoranimated={false}(WCAG 2.4.7).DataTablefooter pagination is now keyboard-operable: the prev/next/number controls use realhrefanchors withpreventDefaultandtabIndexon disabled controls, matchingDataPagination. They were previouslyrole="button"anchors with nohrefand so unreachable by keyboard.
Correctness
AnimatedTestimonialsno longer crashes when thetestimonialsarray shrinks below the active index (the render path now clamps the index and the state reconciles).
@saasflare/ui@3.4.0
Minor Changes
-
11eb992: Add data-driven and wizard components plus Button/overlay parity — all additive, no breaking changes.
New components
DataTable+useDataTable— dependency-free, typed data grid built on the Table primitives: typed column defs, client-side single/multi-column sort, row selection (current-page select-all), client-side pagination, density, sticky header, loading/empty states, and a documentedmanualSort/manualPaginationescape hatch for server-side / TanStack. Sort + select + paginate in ~15 LOC with zero extra dependencies.MultiSelect— searchable multi-select (Popover + cmdk) with chips, select-all,max,+N morecollapse, and async option loading viaonSearchChange. Plainstring[]value.Stepper+useStepper(+StepperNav/StepperPanel/StepperContent) — a controlled multi-step wizard over the existing visualSteps: linear / non-linear flow, optional steps, and an async validation gate before advancing. The headlessuseStepperhook composes with the bareStepsindicator too.DataPagination+paginationSummary— total-driven pagination convenience: prev/next, numbered pages with ellipsis, an optional page-size selector, and an "X–Y of N" summary. One component wires a whole table footer.
API parity additions
Button— new optionalstartContent,endContent,isLoading,isIconOnly, andspinnerPlacementprops (StatefulButtonnow delegates its loading state toButton.isLoading).- Overlays —
DialogContentgainsshowCloseButton(defaults to current behavior);PopoverArrow,TooltipArrow, andHoverCardArrowsub-components added for the floating overlays. Stepgains an additive optionaloptionalprop.
-
11eb992: Make the
SaasflareComponentPropscontract consistent across the catalog, fix verified correctness bugs, and clean up quality issues. Additive — no public API removed or renamed.Contract consistency — every visibly-rendering component now extends
SaasflareComponentPropsand resolvessurface/radius/animated/iconWeightviauseSaasflareProps, emitting thedata-surface/data-radius/data-animatedattributes (coverage went from ~91 to 131 files). Concretely this fixes three systemic gaps:- Motion kill-switch — ~15 JS-motion components (e.g.
HeroVideoDialog,Dock,Timeline,GalleryLightbox,AnimatedTooltip,BentoGridItem) gated only onuseReducedMotion()and silently ignoredanimated={false}; they now honor it viauseSaasflareMotion. data-animatedemission — CSS-motion components (Toggle,Avatar,Sheet,StatCard,TeamCard,TestimonialCard,TagInput,FeatureCard, …) now emit the attribute so the per-component motion gate actually engages.- Off-contract primitives —
Separator,Label,Kbd,Item,ButtonGroup,GradientText, and others now accept the standard axes.
Correctness fixes (44 verified) — including 3
Math.random()SSR-hydration bugs replaced withuseId()(sidebar skeleton,Ratingclip-path, animatedbeamgradient),CommandDialoga11y nesting,AppIcondropping its ref/props,Formhook-order,MouseGradientBlobglobal selector, animatedcursorlistener leak, and missing"use client"onStatCard/TeamCard/Empty/FeatureCard(RSC crash).Quality — replaced hardcoded colors with design tokens (brand colors preserved), swapped reimplemented inline
<svg>icons for Phosphor icons that honoriconWeight(e.g.Stepscheckmark), removed 3 empty stub files, and corrected stale@module/ "Framer Motion" doc labels. Also fixed thesocial-buttonregistry description, which incorrectly advertised 16 providers (it supports 5; the 16-provider component isSocialAuthButton).Documentation — the docs catalog + shadcn registry now also cover 9 previously-undocumented public exports (
Sidebarsystem,SocialAuthButton,StatefulButton,ThemeModeToggle,ThemeModeMultiToggle,UserAvatar,TopLoadingBar,ScrollToTopButton,AnimatedTooltip), so every shipped export now has a props table, demos, and an installable registry block. - Motion kill-switch — ~15 JS-motion components (e.g.
@saasflare/ui@3.3.1
Patch Changes
-
340ddfa: Fix invisible/broken default colors across the effects & motion components. Their
defaults wrapped design tokens inhsl(...)(e.g.hsl(var(--primary))), but the
Saasflare token system is OKLCH —--primary,--chart-1..4,--ring,--border
all resolve to fulloklch(...)colors, sohsl(oklch(...))was invalid CSS and
rendered asnone(transparent). Most visibly,GradientTextproduced invisible
text by default.Affected components now reference the tokens directly (
var(--primary)) and use
color-mix()for alpha:GradientText,Confetti,MouseGradientBlob,
ShimmerButton,SpotlightCard,TracingBeam,BorderBeam,GlowingEffect,
MovingBorder,AuroraBackground,ParticlesBackground,Hotspot,RetroGrid,
Input/Textareafocus ring, and the animatedbeam/shiny-text/cursor
variants. No API changes — defaults and JSDoc examples are corrected in place.
@saasflare/ui@3.3.0
Minor Changes
- 72dd599: Export 36 catalog-expansion components from the main barrel so they are importable from
@saasflare/ui(previously they shipped only via the shadcn registry and were not part of the package's public API):AudioPlayer,BentoGrid/BentoGridItem,BlurFade,BorderBeam,Compare,Confetti,Countdown(+useCountdown),SafariMock/IPhoneMock,Dock/DockItem,FeatureCard,FlipWords,GalleryLightbox,GlowingEffect,GradientText,HeroVideoDialog,Hotspot/HotspotMarker,ImageSwapHover,Marquee,MouseGradientBlob,MovingBorder,PageTransition,ParallaxSection,ParticlesBackground,RetroGrid,RevealOnScroll,ShimmerButton,SocialButton,SpotlightCard,StatCard,Steps/Step,StickyScrollReveal,TeamCard,TestimonialCard,TextGenerateEffect,Timeline/TimelineItem,TracingBeam.
Patch Changes
- 72dd599: Fix JSDoc
@exampleimports across ~70 components. Examples referenced a non-existent@saasflare/corepackage (a 404 on npm); they now correctly import from@saasflare/ui. This corrects the import path surfaced in editor IntelliSense, the docs site, and the MCP server.
@saasflare/ui@3.2.0
Minor Changes
-
3a6e081: Catalog expansion: Tremor-style data viz, new composed widgets, brand auth, and stateful interactions.
New components
- Data viz:
BarList,CategoryBar,Tracker,SparkChart,ProgressCircle - Inputs:
DatePicker,DateRangePicker,NumberInput,Dropzone,TagInput,Rating - Surfaces:
AuroraBackground,Callout,CodeBlock,NotificationCenter,TreeView - Composed:
StatefulButton,ThemeModeMultiToggle - Brand:
SocialAuthButtonand 16 provider presets (GoogleAuthButton,GitHubAuthButton,AppleAuthButton, …) - Icons: internal
Phosphoricon set wired toiconWeightprop
New hooks
useLocalStorage,useMergedRef,useInterval,useFocusTrap,useFileDialog
Theming
- New
aurorasurface variant alongsideflat/glass/clay data-radiusselectors descoped from:rootso component-levelradiusprop now overrides page-wide radius- Palette expansions
Internal
- Drop unused
lucide-reactdependency - Build pipeline now emits component registry + LLM docs alongside
tsupbundle
- Data viz:
@saasflare/ui@3.1.2
TL;DR
Fix: `intent="neutral"` rendered illegibly on transparent-bg variants (Button soft/outline/ghost/link/glass; Badge soft/outline). Also fixes a parallel bug where Button outline/ghost/link with colored intents rendered white text on white pages. Adds a third intent token `--intent-text` to disambiguate the two roles `--intent` was overloaded with.
What was wrong
The `--intent` / `--intent-fg` pair carried two incompatible meanings depending on intent class:
| Intent class | `--intent` | `--intent-fg` |
|---|---|---|
| primary, success, warning, danger, info | saturated brand color (red, blue, …) | white |
| neutral | light gray surface (`--secondary`) | dark gray text |
This made transparent-bg variants illegible for at least one class:
- Button soft / glass + neutral → text used `--intent` = light gray on near-white background, invisible.
- Button outline / ghost / link + colored → text used `--intent-fg` = white on white page, invisible.
- Badge soft / outline + neutral → text + border used `--intent` = light gray, invisible.
What changed
Three tokens per intent now, in `theme.css`:
| Token | Role |
|---|---|
| `--intent` | fill color (used as background in solid/shadow) |
| `--intent-fg` | paired text color readable on top of `--intent` |
| `--intent-text` | text color when rendered without a fill (transparent bg) |
For colored intents `--intent-text` equals `--intent` (the saturated color is itself readable as text on a page background) — zero visual change for solid/soft/shadow variants.
For `neutral`, `--intent-text` falls back to `--secondary-foreground` (dark gray), so transparent-bg variants stay legible.
Variants updated to use `--intent-text` for text/border on transparent or tinted backgrounds:
- `Button`: soft, outline, ghost, link, glass
- `Badge`: soft, outline
`solid` and `shadow` variants are unchanged.
Public API
Untouched. `Intent` type and `data-intent` attribute work exactly as before.
Verification
- npm provenance: ✓ Sigstore SLSA Provenance v1
- npm trusted publishing: ✓ OIDC-signed
- Visual matrix in apps/ui: every (variant × intent) combination now renders legibly
- WCAG AA contrast: passing for both colored intents (e.g. danger outline ≈ 4.6 : 1) and neutral (≈ 6.8 : 1 against light page)
@saasflare/ui@3.1.1
TL;DR
Fix: light-mode `--secondary` was nearly invisible against white backgrounds. Lightness dropped 0.965 → 0.92, chroma multiplier bumped 1× → 1.5×.
What was wrong
`packages/ui/styles/theme.css` had:
```css
--background: oklch(1 0 0); /* L 1.0 (pure white) /
--secondary: oklch(0.965 var(--neutral-c) var(--neutral-h)); / L 0.965 */
```
A perceptual lightness delta of just 0.035 is sub-perceptible to the human eye on white. Every `<Button variant="secondary">`, `bg-secondary` panel, and Soft Badge intent collapsed into the page.
What changed
```css
--secondary: oklch(0.92 calc(var(--neutral-c) * 1.5) var(--neutral-h));
```
Resulting hierarchy on white:
- `--background` 1.0
- `--muted` 0.965 (untouched — its purpose is "barely-different fill")
- `--accent` 0.95 (untouched — subtler interaction surface)
- `--secondary` 0.92 ← changed (clearly visible button/badge fill)
- `--border` 0.915
Secondary now lands at the brightness of shadcn-equivalent `slate-200` — the standard for visible-but-quiet button surfaces. `--secondary-foreground` unchanged at L 0.24 → contrast ratio ≈ 6.8 : 1, passes WCAG AA.
Untouched
- Dark mode (already had 0.11 perceptual separation between bg and secondary)
- `--muted` and `--accent` (different semantic roles)
- All component code — pure CSS token change
Verification
- npm provenance: ✓ Sigstore SLSA Provenance v1
- npm trusted publishing: ✓ OIDC-signed
- Dark mode visual diff: zero change
- WCAG AA contrast: 6.8 : 1 (pass)
@saasflare/ui@3.1.0
TL;DR
Universal compliance: every visible component now respects `surface`, `radius`, and `animated` from `` or per-component override. Before this release, only `Button` did.
What changed
The package's own contract (declared in `useSaasflareProps`) says every Saasflare component MUST extend `SaasflareComponentProps` and resolve via `useSaasflareProps()`. In v3.0 only `Button` followed it. v3.1.0 fixes that for all 55 active-pattern components, including Card, Dialog, Checkbox, Switch, Tabs, Select, DropdownMenu, Tooltip, Popover, all subpath primitives (Calendar, Carousel, Chart, Command, Drawer, InputOTP, Resizable), and all composed SaaS widgets.
Three implementation patterns
| Pattern | Components | Notes |
|---|---|---|
| A Framer-Motion + `useSaasflareMotion` | 19 (Cat 1) | Card, Dialog, Checkbox, Switch, Tabs, Tooltip, etc. |
| B CSS-motion + data-attrs only | 9 (Cat 2) | Toggle, RadioGroup, Toaster, ContextMenu, Menubar, etc. The global CSS layer in `motion.css` zeros out transitions when `data-animated="false"`. |
| C Subset (only applicable props) | 27 (Cat 3+4+5) | Avatar, Spinner, Skeleton, all subpath primitives, all composed widgets. |
Full matrix in `packages/ui/docs/component-compliance.md`.
New helper
```ts
import { useSaasflareMotion, springBouncy } from "@saasflare/ui"
const motion = useSaasflareMotion(animated, springBouncy)
// ↳ { transition: spring | noMotion, disabled: boolean }
```
Single resolver. Combines provider `animated` + OS `prefers-reduced-motion` + variadic extra disablers (e.g. `disabled`, `loading` for buttons). Future per-component spring tokens (`springBouncy.checkbox` etc.) will live next to this hook — components stay unchanged.
Behavior change (technically non-breaking, but observable)
Components that previously ignored `animated={false}` from the provider now respect it consistently. If you relied on per-component animations running despite a global opt-out, set `animated={true}` per-component explicitly:
```tsx
Still animates // explicit override wins
No animation // follows provider
```
Same applies to `surface` and `radius` — components now follow provider context where they previously silently used hardcoded defaults.
Bundle impact
| Metric | v3.0.2 | v3.1.0 | Delta |
|---|---|---|---|
| Main barrel raw | 236 KB | 248 KB | +12 KB |
| Main barrel gzip | 43.4 KB | 44.1 KB | +700 B |
Marginal cost (1.6 % gzip) for universal API consistency across 55 components.
Convention reminders (now documented as spec)
- Spread-first: `{...props}` is always the first attribute on the root, then explicit `data-*` and `className` so Saasflare attributes win over consumer overrides.
- `data-slot` is mandatory: every Saasflare component must set `data-slot=""` on its root. The `prefers-reduced-motion` CSS layer matches `[data-slot]`.
- Compound components: `surface`/`radius`/`animated` go on the visually-rendering subcomponent (`DialogContent`, `AccordionItem`, `PopoverContent`), not on the logical wrapper. Sources can use ``, not ``.
See the full doc for the matrix, patterns, and resolved decisions.
Migration
Required only if you relied on the old "ignored `animated={false}` quirk". Otherwise drop-in.