diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md
index 62dd0c6a9e..30799215b6 100644
--- a/documentation/docs/06-runtime/02-context.md
+++ b/documentation/docs/06-runtime/02-context.md
@@ -22,7 +22,7 @@ export const myGlobalState = $state({
```svelte
```
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index b19cfb355b..4031110fa7 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,17 @@
# svelte
+## 5.8.1
+
+### Patch Changes
+
+- fix: reinstate missing prefersReducedMotion export ([#14586](https://github.com/sveltejs/svelte/pull/14586))
+
+## 5.8.0
+
+### Minor Changes
+
+- feat: add `Spring` and `Tween` classes to `svelte/motion` ([#11519](https://github.com/sveltejs/svelte/pull/11519))
+
## 5.7.1
### Patch Changes
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 6a01e01ecb..c751a598db 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.7.1",
+ "version": "5.8.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js
index d843413e57..92d29d9e1d 100644
--- a/packages/svelte/src/internal/shared/utils.js
+++ b/packages/svelte/src/internal/shared/utils.js
@@ -44,6 +44,27 @@ export function run_all(arr) {
}
}
+/**
+ * TODO replace with Promise.withResolvers once supported widely enough
+ * @template T
+ */
+export function deferred() {
+ /** @type {(value: T) => void} */
+ var resolve;
+
+ /** @type {(reason: any) => void} */
+ var reject;
+
+ /** @type {Promise} */
+ var promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+
+ // @ts-expect-error
+ return { promise, resolve, reject };
+}
+
/**
* @template V
* @param {V} value
diff --git a/packages/svelte/src/motion/private.d.ts b/packages/svelte/src/motion/private.d.ts
index 1a9afb0781..22b8cc4af3 100644
--- a/packages/svelte/src/motion/private.d.ts
+++ b/packages/svelte/src/motion/private.d.ts
@@ -1,9 +1,11 @@
-import { Spring } from './public.js';
-
-export interface TickContext {
+export interface TickContext {
inv_mass: number;
dt: number;
- opts: Spring;
+ opts: {
+ stiffness: number;
+ damping: number;
+ precision: number;
+ };
settled: boolean;
}
@@ -14,8 +16,22 @@ export interface SpringOpts {
}
export interface SpringUpdateOpts {
+ /**
+ * @deprecated Only use this for the spring store; does nothing when set on the Spring class
+ */
hard?: any;
+ /**
+ * @deprecated Only use this for the spring store; does nothing when set on the Spring class
+ */
soft?: string | number | boolean;
+ /**
+ * Only use this for the Spring class; does nothing when set on the spring store
+ */
+ instant?: boolean;
+ /**
+ * Only use this for the Spring class; does nothing when set on the spring store
+ */
+ preserveMomentum?: number;
}
export type Updater = (target_value: T, value: T) => T;
diff --git a/packages/svelte/src/motion/public.d.ts b/packages/svelte/src/motion/public.d.ts
index c0f55a4cca..4e74d4b76f 100644
--- a/packages/svelte/src/motion/public.d.ts
+++ b/packages/svelte/src/motion/public.d.ts
@@ -1,17 +1,88 @@
-import { Readable } from '../store/public.js';
-import { SpringUpdateOpts, TweenedOptions, Updater } from './private.js';
+import { Readable, type Unsubscriber } from '../store/public.js';
+import { SpringUpdateOpts, TweenedOptions, Updater, SpringOpts } from './private.js';
+
+// TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface)
+// this means both the Spring class and the Spring interface are merged into one with some things only
+// existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js
export interface Spring extends Readable {
- set: (new_value: T, opts?: SpringUpdateOpts) => Promise;
+ set(new_value: T, opts?: SpringUpdateOpts): Promise;
+ /**
+ * @deprecated Only exists on the legacy `spring` store, not the `Spring` class
+ */
update: (fn: Updater, opts?: SpringUpdateOpts) => Promise;
+ /**
+ * @deprecated Only exists on the legacy `spring` store, not the `Spring` class
+ */
+ subscribe(fn: (value: T) => void): Unsubscriber;
precision: number;
damping: number;
stiffness: number;
}
+/**
+ * A wrapper for a value that behaves in a spring-like fashion. Changes to `spring.target` will cause `spring.current` to
+ * move towards it over time, taking account of the `spring.stiffness` and `spring.damping` parameters.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ * ```
+ * @since 5.8.0
+ */
+export class Spring {
+ constructor(value: T, options?: SpringOpts);
+
+ /**
+ * Create a spring whose value is bound to the return value of `fn`. This must be called
+ * inside an effect root (for example, during component initialisation).
+ *
+ * ```svelte
+ *
+ * ```
+ */
+ static of(fn: () => U, options?: SpringOpts): Spring;
+
+ /**
+ * Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
+ *
+ * If `options.instant` is `true`, `spring.current` immediately matches `spring.target`.
+ *
+ * If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for
+ * the specified number of milliseconds. This is useful for things like 'fling' gestures.
+ */
+ set(value: T, options?: SpringUpdateOpts): Promise;
+
+ damping: number;
+ precision: number;
+ stiffness: number;
+ /**
+ * The end value of the spring.
+ * This property only exists on the `Spring` class, not the legacy `spring` store.
+ */
+ target: T;
+ /**
+ * The current value of the spring.
+ * This property only exists on the `Spring` class, not the legacy `spring` store.
+ */
+ get current(): T;
+}
+
export interface Tweened extends Readable {
set(value: T, opts?: TweenedOptions): Promise;
update(updater: Updater, opts?: TweenedOptions): Promise;
}
-export * from './index.js';
+export { prefersReducedMotion, spring, tweened, Tween } from './index.js';
diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js
index 992085b4b5..270fabd4c7 100644
--- a/packages/svelte/src/motion/spring.js
+++ b/packages/svelte/src/motion/spring.js
@@ -1,14 +1,18 @@
/** @import { Task } from '#client' */
/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */
-/** @import { Spring } from './public.js' */
+/** @import { Spring as SpringStore } from './public.js' */
import { writable } from '../store/shared/index.js';
import { loop } from '../internal/client/loop.js';
import { raf } from '../internal/client/timing.js';
import { is_date } from './utils.js';
+import { set, source } from '../internal/client/reactivity/sources.js';
+import { render_effect } from '../internal/client/reactivity/effects.js';
+import { get } from '../internal/client/runtime.js';
+import { deferred, noop } from '../internal/shared/utils.js';
/**
* @template T
- * @param {TickContext} ctx
+ * @param {TickContext} ctx
* @param {T} last_value
* @param {T} current_value
* @param {T} target_value
@@ -53,10 +57,11 @@ function tick_spring(ctx, last_value, current_value, target_value) {
/**
* The spring function in Svelte creates a store whose value is animated, with a motion that simulates the behavior of a spring. This means when the value changes, instead of transitioning at a steady rate, it "bounces" like a spring would, depending on the physics parameters provided. This adds a level of realism to the transitions and can enhance the user experience.
*
+ * @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
* @template [T=any]
* @param {T} [value]
* @param {SpringOpts} [opts]
- * @returns {Spring}
+ * @returns {SpringStore}
*/
export function spring(value, opts = {}) {
const store = writable(value);
@@ -103,7 +108,7 @@ export function spring(value, opts = {}) {
return false;
}
inv_mass = Math.min(inv_mass + inv_mass_recovery_rate, 1);
- /** @type {TickContext} */
+ /** @type {TickContext} */
const ctx = {
inv_mass,
opts: spring,
@@ -127,7 +132,8 @@ export function spring(value, opts = {}) {
});
});
}
- /** @type {Spring} */
+ /** @type {SpringStore} */
+ // @ts-expect-error - class-only properties are missing
const spring = {
set,
update: (fn, opts) => set(fn(/** @type {T} */ (target_value), /** @type {T} */ (value)), opts),
@@ -138,3 +144,205 @@ export function spring(value, opts = {}) {
};
return spring;
}
+
+/**
+ * A wrapper for a value that behaves in a spring-like fashion. Changes to `spring.target` will cause `spring.current` to
+ * move towards it over time, taking account of the `spring.stiffness` and `spring.damping` parameters.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ * ```
+ * @template T
+ * @since 5.8.0
+ */
+export class Spring {
+ #stiffness = source(0.15);
+ #damping = source(0.8);
+ #precision = source(0.01);
+
+ #current = source(/** @type {T} */ (undefined));
+ #target = source(/** @type {T} */ (undefined));
+
+ #last_value = /** @type {T} */ (undefined);
+ #last_time = 0;
+
+ #inverse_mass = 1;
+ #momentum = 0;
+
+ /** @type {import('../internal/client/types').Task | null} */
+ #task = null;
+
+ /** @type {ReturnType | null} */
+ #deferred = null;
+
+ /**
+ * @param {T} value
+ * @param {SpringOpts} [options]
+ */
+ constructor(value, options = {}) {
+ this.#current.v = this.#target.v = value;
+
+ if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1);
+ if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1);
+ if (typeof options.precision === 'number') this.#precision.v = options.precision;
+ }
+
+ /**
+ * Create a spring whose value is bound to the return value of `fn`. This must be called
+ * inside an effect root (for example, during component initialisation).
+ *
+ * ```svelte
+ *
+ * ```
+ * @template U
+ * @param {() => U} fn
+ * @param {SpringOpts} [options]
+ */
+ static of(fn, options) {
+ const spring = new Spring(fn(), options);
+
+ render_effect(() => {
+ spring.set(fn());
+ });
+
+ return spring;
+ }
+
+ /** @param {T} value */
+ #update(value) {
+ set(this.#target, value);
+
+ this.#current.v ??= value;
+ this.#last_value ??= this.#current.v;
+
+ if (!this.#task) {
+ this.#last_time = raf.now();
+
+ var inv_mass_recovery_rate = 1000 / (this.#momentum * 60);
+
+ this.#task ??= loop((now) => {
+ this.#inverse_mass = Math.min(this.#inverse_mass + inv_mass_recovery_rate, 1);
+
+ /** @type {import('./private').TickContext} */
+ const ctx = {
+ inv_mass: this.#inverse_mass,
+ opts: {
+ stiffness: this.#stiffness.v,
+ damping: this.#damping.v,
+ precision: this.#precision.v
+ },
+ settled: true,
+ dt: ((now - this.#last_time) * 60) / 1000
+ };
+
+ var next = tick_spring(ctx, this.#last_value, this.#current.v, this.#target.v);
+ this.#last_value = this.#current.v;
+ this.#last_time = now;
+ set(this.#current, next);
+
+ if (ctx.settled) {
+ this.#task = null;
+ }
+
+ return !ctx.settled;
+ });
+ }
+
+ return this.#task.promise;
+ }
+
+ /**
+ * Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
+ *
+ * If `options.instant` is `true`, `spring.current` immediately matches `spring.target`.
+ *
+ * If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for
+ * the specified number of milliseconds. This is useful for things like 'fling' gestures.
+ *
+ * @param {T} value
+ * @param {SpringUpdateOpts} [options]
+ */
+ set(value, options) {
+ this.#deferred?.reject(new Error('Aborted'));
+
+ if (options?.instant || this.#current.v === undefined) {
+ this.#task?.abort();
+ this.#task = null;
+ set(this.#current, set(this.#target, value));
+ return Promise.resolve();
+ }
+
+ if (options?.preserveMomentum) {
+ this.#inverse_mass = 0;
+ this.#momentum = options.preserveMomentum;
+ }
+
+ var d = (this.#deferred = deferred());
+ d.promise.catch(noop);
+
+ this.#update(value).then(() => {
+ if (d !== this.#deferred) return;
+ d.resolve(undefined);
+ });
+
+ return d.promise;
+ }
+
+ get current() {
+ return get(this.#current);
+ }
+
+ get damping() {
+ return get(this.#damping);
+ }
+
+ set damping(v) {
+ set(this.#damping, clamp(v, 0, 1));
+ }
+
+ get precision() {
+ return get(this.#precision);
+ }
+
+ set precision(v) {
+ set(this.#precision, v);
+ }
+
+ get stiffness() {
+ return get(this.#stiffness);
+ }
+
+ set stiffness(v) {
+ set(this.#stiffness, clamp(v, 0, 1));
+ }
+
+ get target() {
+ return get(this.#target);
+ }
+
+ set target(v) {
+ this.set(v);
+ }
+}
+
+/**
+ * @param {number} n
+ * @param {number} min
+ * @param {number} max
+ */
+function clamp(n, min, max) {
+ return Math.max(min, Math.min(max, n));
+}
diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js
index 8f689878f7..d732dbc283 100644
--- a/packages/svelte/src/motion/tweened.js
+++ b/packages/svelte/src/motion/tweened.js
@@ -6,6 +6,8 @@ import { raf } from '../internal/client/timing.js';
import { loop } from '../internal/client/loop.js';
import { linear } from '../easing/index.js';
import { is_date } from './utils.js';
+import { set, source } from '../internal/client/reactivity/sources.js';
+import { get, render_effect } from 'svelte/internal/client';
/**
* @template T
@@ -76,6 +78,7 @@ function get_interpolator(a, b) {
/**
* A tweened store in Svelte is a special type of store that provides smooth transitions between state values over time.
*
+ * @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead
* @template T
* @param {T} [value]
* @param {TweenedOptions} [defaults]
@@ -152,3 +155,137 @@ export function tweened(value, defaults = {}) {
subscribe: store.subscribe
};
}
+
+/**
+ * A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to
+ * move towards it over time, taking account of the `delay`, `duration` and `easing` options.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ * ```
+ * @template T
+ * @since 5.8.0
+ */
+export class Tween {
+ #current = source(/** @type {T} */ (undefined));
+ #target = source(/** @type {T} */ (undefined));
+
+ /** @type {TweenedOptions} */
+ #defaults;
+
+ /** @type {import('../internal/client/types').Task | null} */
+ #task = null;
+
+ /**
+ * @param {T} value
+ * @param {TweenedOptions} options
+ */
+ constructor(value, options = {}) {
+ this.#current.v = this.#target.v = value;
+ this.#defaults = options;
+ }
+
+ /**
+ * Create a tween whose value is bound to the return value of `fn`. This must be called
+ * inside an effect root (for example, during component initialisation).
+ *
+ * ```svelte
+ *
+ * ```
+ * @template U
+ * @param {() => U} fn
+ * @param {TweenedOptions} [options]
+ */
+ static of(fn, options) {
+ const tween = new Tween(fn(), options);
+
+ render_effect(() => {
+ tween.set(fn());
+ });
+
+ return tween;
+ }
+
+ /**
+ * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it.
+ *
+ * If `options` are provided, they will override the tween's defaults.
+ * @param {T} value
+ * @param {TweenedOptions} [options]
+ * @returns
+ */
+ set(value, options) {
+ set(this.#target, value);
+
+ let previous_value = this.#current.v;
+ let previous_task = this.#task;
+
+ let started = false;
+ let {
+ delay = 0,
+ duration = 400,
+ easing = linear,
+ interpolate = get_interpolator
+ } = { ...this.#defaults, ...options };
+
+ const start = raf.now() + delay;
+
+ /** @type {(t: number) => T} */
+ let fn;
+
+ this.#task = loop((now) => {
+ if (now < start) {
+ return true;
+ }
+
+ if (!started) {
+ started = true;
+
+ fn = interpolate(/** @type {any} */ (previous_value), value);
+
+ if (typeof duration === 'function') {
+ duration = duration(/** @type {any} */ (previous_value), value);
+ }
+
+ previous_task?.abort();
+ }
+
+ const elapsed = now - start;
+
+ if (elapsed > /** @type {number} */ (duration)) {
+ set(this.#current, value);
+ return false;
+ }
+
+ set(this.#current, fn(easing(elapsed / /** @type {number} */ (duration))));
+ return true;
+ });
+
+ return this.#task.promise;
+ }
+
+ get current() {
+ return get(this.#current);
+ }
+
+ get target() {
+ return get(this.#target);
+ }
+
+ set target(v) {
+ this.set(v);
+ }
+}
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 331506a55a..3061318cb0 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
-export const VERSION = '5.7.1';
+export const VERSION = '5.8.1';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index f8ce7b9965..3e10aa1f6b 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1644,14 +1644,85 @@ declare module 'svelte/legacy' {
declare module 'svelte/motion' {
import type { MediaQuery } from 'svelte/reactivity';
+ // TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface)
+ // this means both the Spring class and the Spring interface are merged into one with some things only
+ // existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js
+
export interface Spring extends Readable {
- set: (new_value: T, opts?: SpringUpdateOpts) => Promise;
+ set(new_value: T, opts?: SpringUpdateOpts): Promise;
+ /**
+ * @deprecated Only exists on the legacy `spring` store, not the `Spring` class
+ */
update: (fn: Updater, opts?: SpringUpdateOpts) => Promise;
+ /**
+ * @deprecated Only exists on the legacy `spring` store, not the `Spring` class
+ */
+ subscribe(fn: (value: T) => void): Unsubscriber;
precision: number;
damping: number;
stiffness: number;
}
+ /**
+ * A wrapper for a value that behaves in a spring-like fashion. Changes to `spring.target` will cause `spring.current` to
+ * move towards it over time, taking account of the `spring.stiffness` and `spring.damping` parameters.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ * ```
+ * @since 5.8.0
+ */
+ export class Spring {
+ constructor(value: T, options?: SpringOpts);
+
+ /**
+ * Create a spring whose value is bound to the return value of `fn`. This must be called
+ * inside an effect root (for example, during component initialisation).
+ *
+ * ```svelte
+ *
+ * ```
+ */
+ static of(fn: () => U, options?: SpringOpts): Spring;
+
+ /**
+ * Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
+ *
+ * If `options.instant` is `true`, `spring.current` immediately matches `spring.target`.
+ *
+ * If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for
+ * the specified number of milliseconds. This is useful for things like 'fling' gestures.
+ */
+ set(value: T, options?: SpringUpdateOpts): Promise;
+
+ damping: number;
+ precision: number;
+ stiffness: number;
+ /**
+ * The end value of the spring.
+ * This property only exists on the `Spring` class, not the legacy `spring` store.
+ */
+ target: T;
+ /**
+ * The current value of the spring.
+ * This property only exists on the `Spring` class, not the legacy `spring` store.
+ */
+ get current(): T;
+ }
+
export interface Tweened extends Readable {
set(value: T, opts?: TweenedOptions): Promise;
update(updater: Updater, opts?: TweenedOptions): Promise;
@@ -1678,8 +1749,22 @@ declare module 'svelte/motion' {
}
interface SpringUpdateOpts {
+ /**
+ * @deprecated Only use this for the spring store; does nothing when set on the Spring class
+ */
hard?: any;
+ /**
+ * @deprecated Only use this for the spring store; does nothing when set on the Spring class
+ */
soft?: string | number | boolean;
+ /**
+ * Only use this for the Spring class; does nothing when set on the spring store
+ */
+ instant?: boolean;
+ /**
+ * Only use this for the Spring class; does nothing when set on the spring store
+ */
+ preserveMomentum?: number;
}
type Updater = (target_value: T, value: T) => T;
@@ -1717,13 +1802,61 @@ declare module 'svelte/motion' {
/**
* The spring function in Svelte creates a store whose value is animated, with a motion that simulates the behavior of a spring. This means when the value changes, instead of transitioning at a steady rate, it "bounces" like a spring would, depending on the physics parameters provided. This adds a level of realism to the transitions and can enhance the user experience.
*
+ * @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
* */
export function spring(value?: T | undefined, opts?: SpringOpts | undefined): Spring;
/**
* A tweened store in Svelte is a special type of store that provides smooth transitions between state values over time.
*
+ * @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead
* */
export function tweened(value?: T | undefined, defaults?: TweenedOptions | undefined): Tweened;
+ /**
+ * A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to
+ * move towards it over time, taking account of the `delay`, `duration` and `easing` options.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ * ```
+ * @since 5.8.0
+ */
+ export class Tween {
+ /**
+ * Create a tween whose value is bound to the return value of `fn`. This must be called
+ * inside an effect root (for example, during component initialisation).
+ *
+ * ```svelte
+ *
+ * ```
+ *
+ */
+ static of(fn: () => U, options?: TweenedOptions | undefined): Tween;
+
+ constructor(value: T, options?: TweenedOptions);
+ /**
+ * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it.
+ *
+ * If `options` are provided, they will override the tween's defaults.
+ * */
+ set(value: T, options?: TweenedOptions | undefined): Promise;
+ get current(): T;
+ set target(v: T);
+ get target(): T;
+ #private;
+ }
export {};
}