Skip to content

Releases: saasflare/saasflare-ui

@saasflare/ui@3.7.0

Choose a tag to compare

@github-actions github-actions released this 11 Jun 22:24
73c1904

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), including cn, PALETTES, and the type/constant modules. The main barrel itself carries no directive anymore. createSafeContext moved from lib/utils into 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 without sideEffects hints, and to near-zero in bundlers that honor the package's sideEffects field.
    • Peer confinement: react-hook-form is referenced only by form.* and react-day-picker only by calendar.* — no longer baked into the shared entry chunk. (They remain required peers for now: CJS require of 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.

  • 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 failed tsc), and maxWait re-arms per burst instead of firing once per component lifetime.
      • useIntersectionObserver: inline array threshold (the documented usage) no longer re-creates the observer in an infinite loop; observer callback reads the last entry of a batch.
      • useEventListener: inline options objects no longer tear the listener down every render (deps key on option primitives) — fixes broken once semantics.
      • useLongPress: pending long-press timer cleared on unmount; latest-ref writes moved out of the render body.
      • useScrollLock: restores the body's original padding-right instead of clobbering it.
      • useClipboard: reset timer cleared on unmount.
    • sonner toast fix: sonner is no longer inlined into the bundle (noExternal removed) — the inlined copy had its own toast state, so consumers' toast() calls never rendered in the package's <Toaster>. toast is now also re-exported from @saasflare/ui so 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.
  • 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 layout prop, which is silently inert under the domAnimation LazyMotion bundle SaasflareShell loads (layout projection lives in domMax) — 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 CSS translate-x transform sized per data-size, honoring animated={false} and prefers-reduced-motion.
    • MultiSelect: removed the equally-inert layout prop 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 onComplete callback no longer restarts the animation on every parent render (latest-ref; effects keyed on real triggers only).
    • Peers made honest: react-hook-form and react-day-picker are 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.
  • 6abccab: Strict-review fix batch — correctness, a11y, and contract repairs across the package.

    Behavioral fix (flagged): MultiSelect.closeOnSelect semantics were inverted relative to the prop name (true kept the popover open). The prop now means what it says — true closes on each pick — and the default flipped to false, so default behavior is unchanged. Only consumers who passed closeOnSelect explicitly 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 — adds saasflare (house palette), lavender, mint, sage, sky, snow.
    • useFileDialog no longer pins first-call onChange/accept/multiple/capture/directory forever (options re-applied on every open(), 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 expose data-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 undefined emitted on deselect no longer flips the component to uncontrolled), and value accepts null for controlled-empty.
    • Size axis unified on "md": Avatar, Switch, NativeSelect, SelectTrigger, Toggle, ToggleGroup, Item, SidebarMenuButton, ThemeModeMultiToggle migrate "default""md" (canonical Size scale). "default" remains accepted as a deprecated alias.
    • SocialButton deprecated in favor of SocialAuthButton; its conflicting SocialProvider type renamed to SocialButtonProvider (the package-level SocialProvider export 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 stale themes.css reference in the styles entry docs.

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 getBoundingClientRect per mousemove (invalidated on enter/scroll/resize).
    • MouseGradientBlob: same rect caching in the mousemove hot path.
    • DockItem: item centers are cached instead of reading getBoundingClientRect for 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 getElementById every frame forever when the id never mounts.
    • Docs: Logo and AppIcon join the catalog (registry, props, doc pages, demos) — the generated docs now cover all 130 public components.
  • 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 closeOnSelect semantics, DatePicker controlled clear (incl. null), TagInput separator paste, useDebouncedCallback maxWait re-arm + concrete-callback typing, useCountdown SSR-safe first paint + expired-target timer, useFileDialog option re-application + unmount cleanup, useScrollLock padding restore, and the 26-palette PALETTES lockstep.
    • GitHub Act...
Read more

@saasflare/ui@3.6.0

Choose a tag to compare

Minor Changes

  • ec4a289: Consistency, a11y, and correctness fixes across the component set.

    iconWeight no longer leaks to the DOM

    • ~50 components extended SaasflareComponentProps (which includes iconWeight) but only forwarded surface/radius/animated to the resolver, letting iconWeight fall through ...props onto a DOM/Radix/motion element — emitting an unknown iconWeight attribute (React 19 warning) for anyone who set it. Every affected component now consumes iconWeight via the resolver, so the axis is honored and never reaches the DOM.

    Theming / API

    • AlertDialogContent and SheetContent now honor the surface axis (use surface-card, matching Dialog/Drawer) instead of a hardcoded bg-background — the surface prop they already advertised now actually renders.
    • Badge variant now accepts the deprecated aliases "default" | "destructive" | "secondary" in its type (previously a runtime-only compat map that TypeScript users could not reach).
    • DockItemProps no longer extends SaasflareComponentProps — it advertised surface/radius/animated/iconWeight but ignored them (a DockItem inherits motion from its parent Dock via context). Type-narrowing note: if you passed these no-op props to DockItem, remove them.

    Correctness

    • Dock magnification used pageX against viewport-space bounds, so items magnified at the wrong position once the page scrolled horizontally — now uses clientX.
    • SparkChart variant="bar" computed negative bar widths past ~40 points (bars vanished); switched to a proportional slot layout that scales to any point count.
    • Dropzone drag-active state no longer flickers as the cursor crosses child elements (drag-depth counter instead of a naive dragleave).
    • TimelineItem renders a plain <div> when motion is disabled, so motion-off consumers don't instantiate a motion node per item or require the LazyMotion provider just for a timeline.

    Accessibility

    • FlipWords announces word changes — the aria-live region moved from the per-cycle keyed node (never announced) to the stable container.
    • AnimatedTooltip avatars are now keyboard-focusable and reveal their tooltip on focus, with an accessible label.
    • Rating exposes aria-valuetext (e.g. "4.5 of 5 stars") so half-star values are announced with context.
    • Dock items reveal their tooltip on keyboard focus, not just hover.
    • Stepper no longer sets aria-current="step" on two nested elements — the decorative Steps indicator is hidden from assistive tech, leaving the interactive trigger buttons as the single source.

@saasflare/ui@3.5.0

Choose a tag to compare

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 the pointer-events-none overlay, so the listeners never fired and the glow stayed inert. Tracking now runs on the positioned parent the overlay covers (the same pattern SpotlightCard already uses); the overlay stays pointer-events-none.
    • MouseGradientBlob: the mousemove listener was bound to the component's own pointer-events-none root, so the blob never moved. It now binds to the positioned host element.

    New public exports (previously documented but unreachable)

    • Logo / AppIcon (and LogoProps / AppIconProps) are now exported from the package root. Their JSDoc advertised import { Logo } from '@saasflare/ui', but they were never re-exported, so the documented import threw.

    Accessibility

    • GalleryLightbox now traps focus and locks body scroll via the shared useFocusTrap / useScrollLock hooks (matching HeroVideoDialog), with focus moved in on open and restored on close. Previously it was an aria-modal dialog with no focus management.
    • Input / Textarea now render a motion-independent CSS focus ring (focus-visible:ring-[3px] ring-ring/50), matching NativeSelect/SearchField/Select. The focus ring no longer disappears under prefers-reduced-motion or animated={false} (WCAG 2.4.7).
    • DataTable footer pagination is now keyboard-operable: the prev/next/number controls use real href anchors with preventDefault and tabIndex on disabled controls, matching DataPagination. They were previously role="button" anchors with no href and so unreachable by keyboard.

    Correctness

    • AnimatedTestimonials no longer crashes when the testimonials array shrinks below the active index (the render path now clamps the index and the state reconciles).

@saasflare/ui@3.4.0

Choose a tag to compare

@github-actions github-actions released this 02 Jun 09:06
4e933b5

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 documented manualSort/manualPagination escape 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 more collapse, and async option loading via onSearchChange. Plain string[] value.
    • Stepper + useStepper (+ StepperNav / StepperPanel / StepperContent) — a controlled multi-step wizard over the existing visual Steps: linear / non-linear flow, optional steps, and an async validation gate before advancing. The headless useStepper hook composes with the bare Steps indicator 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 optional startContent, endContent, isLoading, isIconOnly, and spinnerPlacement props (StatefulButton now delegates its loading state to Button.isLoading).
    • OverlaysDialogContent gains showCloseButton (defaults to current behavior); PopoverArrow, TooltipArrow, and HoverCardArrow sub-components added for the floating overlays.
    • Step gains an additive optional optional prop.
  • 11eb992: Make the SaasflareComponentProps contract 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 SaasflareComponentProps and resolves surface / radius / animated / iconWeight via useSaasflareProps, emitting the data-surface / data-radius / data-animated attributes (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 on useReducedMotion() and silently ignored animated={false}; they now honor it via useSaasflareMotion.
    • data-animated emission — 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 primitivesSeparator, Label, Kbd, Item, ButtonGroup, GradientText, and others now accept the standard axes.

    Correctness fixes (44 verified) — including 3 Math.random() SSR-hydration bugs replaced with useId() (sidebar skeleton, Rating clip-path, animated beam gradient), CommandDialog a11y nesting, AppIcon dropping its ref/props, Form hook-order, MouseGradientBlob global selector, animated cursor listener leak, and missing "use client" on StatCard / TeamCard / Empty / FeatureCard (RSC crash).

    Quality — replaced hardcoded colors with design tokens (brand colors preserved), swapped reimplemented inline <svg> icons for Phosphor icons that honor iconWeight (e.g. Steps checkmark), removed 3 empty stub files, and corrected stale @module / "Framer Motion" doc labels. Also fixed the social-button registry description, which incorrectly advertised 16 providers (it supports 5; the 16-provider component is SocialAuthButton).

    Documentation — the docs catalog + shadcn registry now also cover 9 previously-undocumented public exports (Sidebar system, SocialAuthButton, StatefulButton, ThemeModeToggle, ThemeModeMultiToggle, UserAvatar, TopLoadingBar, ScrollToTopButton, AnimatedTooltip), so every shipped export now has a props table, demos, and an installable registry block.

@saasflare/ui@3.3.1

Choose a tag to compare

@github-actions github-actions released this 30 May 10:47
559507c

Patch Changes

  • 340ddfa: Fix invisible/broken default colors across the effects & motion components. Their
    defaults wrapped design tokens in hsl(...) (e.g. hsl(var(--primary))), but the
    Saasflare token system is OKLCH — --primary, --chart-1..4, --ring, --border
    all resolve to full oklch(...) colors, so hsl(oklch(...)) was invalid CSS and
    rendered as none (transparent). Most visibly, GradientText produced 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/Textarea focus ring, and the animated beam/shiny-text/cursor
    variants. No API changes — defaults and JSDoc examples are corrected in place.

@saasflare/ui@3.3.0

Choose a tag to compare

@github-actions github-actions released this 30 May 10:38
f94d2b8

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 @example imports across ~70 components. Examples referenced a non-existent @saasflare/core package (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

Choose a tag to compare

@github-actions github-actions released this 27 May 08:39
1108d35

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: SocialAuthButton and 16 provider presets (GoogleAuthButton, GitHubAuthButton, AppleAuthButton, …)
    • Icons: internal Phosphor icon set wired to iconWeight prop

    New hooks

    • useLocalStorage, useMergedRef, useInterval, useFocusTrap, useFileDialog

    Theming

    • New aurora surface variant alongside flat/glass/clay
    • data-radius selectors descoped from :root so component-level radius prop now overrides page-wide radius
    • Palette expansions

    Internal

    • Drop unused lucide-react dependency
    • Build pipeline now emits component registry + LLM docs alongside tsup bundle

@saasflare/ui@3.1.2

Choose a tag to compare

@github-actions github-actions released this 07 May 13:51
7b9ebef

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

Choose a tag to compare

@github-actions github-actions released this 07 May 13:31
0dfbe69

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

Choose a tag to compare

@github-actions github-actions released this 07 May 11:07
2f37632

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.

Verification

  • npm provenance: ✓ Sigstore SLSA Provenance v1
  • npm trusted publishing: ✓ OIDC-signed
  • Build green across 5 PRs (#13#17), each phase isolated for review