@humanspeak/svelte-motion 0.2.1 → 0.3.1
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/dist/components/MotionConfig.svelte +18 -4
- package/dist/html/_MotionContainer.svelte +32 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +18 -0
- package/dist/utils/reducedMotion.d.ts +20 -0
- package/dist/utils/reducedMotion.js +42 -0
- package/dist/utils/reducedMotionConfig.d.ts +39 -0
- package/dist/utils/reducedMotionConfig.js +92 -0
- package/package.json +1 -1
|
@@ -6,15 +6,29 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Provide default Motion configuration to descendants.
|
|
8
8
|
*
|
|
9
|
-
* Wraps content and supplies defaults such as `transition`
|
|
10
|
-
* with per-element props. Descendants can
|
|
9
|
+
* Wraps content and supplies defaults such as `transition` and
|
|
10
|
+
* `reducedMotion` that are merged with per-element props. Descendants can
|
|
11
|
+
* retrieve config via context.
|
|
11
12
|
*
|
|
12
13
|
* @prop transition Default `AnimationOptions` merged with element props.
|
|
14
|
+
* @prop reducedMotion Reduced-motion policy: `'user' | 'always' | 'never'`.
|
|
15
|
+
* Defaults to `'never'`.
|
|
13
16
|
* @prop children Slotted content receiving this configuration.
|
|
14
17
|
*/
|
|
15
|
-
let { transition, children }: MotionConfigProps & { children?: Snippet } =
|
|
18
|
+
let { transition, reducedMotion, children }: MotionConfigProps & { children?: Snippet } =
|
|
19
|
+
$props()
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
// Use property getters so descendants always read the parent's current
|
|
22
|
+
// prop values — including remounted children inside `{#key}` blocks, which
|
|
23
|
+
// would otherwise see a stale snapshot if we cached the value in $state.
|
|
24
|
+
const motionConfig: MotionConfigProps = {
|
|
25
|
+
get transition() {
|
|
26
|
+
return transition
|
|
27
|
+
},
|
|
28
|
+
get reducedMotion() {
|
|
29
|
+
return reducedMotion
|
|
30
|
+
}
|
|
31
|
+
}
|
|
18
32
|
createMotionConfig(motionConfig)
|
|
19
33
|
</script>
|
|
20
34
|
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
<script lang="ts">
|
|
7
7
|
import { getMotionConfig } from '../components/motionConfig.context'
|
|
8
|
+
import {
|
|
9
|
+
filterReducedMotionKeyframes,
|
|
10
|
+
useReducedMotionConfig
|
|
11
|
+
} from '../utils/reducedMotionConfig'
|
|
8
12
|
import type {
|
|
9
13
|
MotionProps,
|
|
10
14
|
MotionTransition,
|
|
@@ -50,7 +54,7 @@
|
|
|
50
54
|
setInitialFalseContext,
|
|
51
55
|
getInitialFalseContext
|
|
52
56
|
} from '../components/variantContext.context'
|
|
53
|
-
import { writable } from 'svelte/store'
|
|
57
|
+
import { get, writable } from 'svelte/store'
|
|
54
58
|
import {
|
|
55
59
|
transformSVGPathProperties,
|
|
56
60
|
computeNormalizedSVGInitialAttrs,
|
|
@@ -115,6 +119,11 @@
|
|
|
115
119
|
let isLoaded = $state<'mounting' | 'initial' | 'ready' | 'animated'>('mounting')
|
|
116
120
|
let dataPath = $state<number>(-1)
|
|
117
121
|
const motionConfig = $derived(getMotionConfig())
|
|
122
|
+
const reducedMotionStore = useReducedMotionConfig()
|
|
123
|
+
// Seed synchronously so the first render filters keyframes correctly —
|
|
124
|
+
// otherwise transforms could flash before the subscribe effect runs.
|
|
125
|
+
let reducedMotion = $state(get(reducedMotionStore))
|
|
126
|
+
$effect(() => reducedMotionStore.subscribe((value) => (reducedMotion = value)))
|
|
118
127
|
|
|
119
128
|
// Get presence context to check if we're inside AnimatePresence
|
|
120
129
|
const context = getAnimatePresenceContext()
|
|
@@ -219,10 +228,14 @@
|
|
|
219
228
|
// Reactively update registration when element/exit/transition props change
|
|
220
229
|
$effect(() => {
|
|
221
230
|
if (element && context && resolvedExit) {
|
|
231
|
+
const filteredExit = filterReducedMotionKeyframes(
|
|
232
|
+
resolvedExit as Record<string, unknown>,
|
|
233
|
+
reducedMotion
|
|
234
|
+
)
|
|
222
235
|
context.registerChild(
|
|
223
236
|
presenceKey,
|
|
224
237
|
element,
|
|
225
|
-
|
|
238
|
+
filteredExit,
|
|
226
239
|
mergedTransition as unknown as MotionTransition
|
|
227
240
|
)
|
|
228
241
|
}
|
|
@@ -350,7 +363,12 @@
|
|
|
350
363
|
const resolvedExit = $derived(resolveExit(exitProp, variantsProp))
|
|
351
364
|
|
|
352
365
|
// Extract keyframes from resolved initial, handling initial={false}
|
|
353
|
-
const initialKeyframes = $derived(
|
|
366
|
+
const initialKeyframes = $derived(
|
|
367
|
+
filterReducedMotionKeyframes(
|
|
368
|
+
getInitialKeyframes(resolvedInitial) as Record<string, unknown>,
|
|
369
|
+
reducedMotion
|
|
370
|
+
)
|
|
371
|
+
)
|
|
354
372
|
|
|
355
373
|
// Derived attributes to keep both branches in sync (focusability, data flags, style, class)
|
|
356
374
|
const derivedAttrs = $derived<Record<string, unknown>>({
|
|
@@ -492,6 +510,13 @@
|
|
|
492
510
|
payload as Record<string, unknown>
|
|
493
511
|
) as typeof payload
|
|
494
512
|
|
|
513
|
+
// Strip transform keys when reduced-motion is active so the element
|
|
514
|
+
// stays in place while opacity / color etc. still animate.
|
|
515
|
+
payload = filterReducedMotionKeyframes(
|
|
516
|
+
payload as Record<string, unknown>,
|
|
517
|
+
reducedMotion
|
|
518
|
+
) as typeof payload
|
|
519
|
+
|
|
495
520
|
// Ensure dash properties aren't pinned as inline styles
|
|
496
521
|
if (element && (element as HTMLElement).style) {
|
|
497
522
|
;(element as HTMLElement).style.removeProperty('stroke-dasharray')
|
|
@@ -801,7 +826,10 @@
|
|
|
801
826
|
try {
|
|
802
827
|
// 1. Run exit animation if defined
|
|
803
828
|
if (resolvedExit && element && !keyTransitionStopped) {
|
|
804
|
-
const exitKeyframes =
|
|
829
|
+
const exitKeyframes = filterReducedMotionKeyframes(
|
|
830
|
+
{ ...(resolvedExit as Record<string, unknown>) },
|
|
831
|
+
reducedMotion
|
|
832
|
+
)
|
|
805
833
|
// Remove transition from keyframes (it's passed separately)
|
|
806
834
|
delete exitKeyframes.transition
|
|
807
835
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,13 +4,15 @@ export { motion } from './motion';
|
|
|
4
4
|
export { animate, delay, hover, inView, press, resize, scroll, stagger, transform } from 'motion';
|
|
5
5
|
export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cubicBezier, easeIn, easeInOut, easeOut } from 'motion';
|
|
6
6
|
export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
|
|
7
|
-
export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, Variants } from './types';
|
|
7
|
+
export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ReducedMotionConfig, Variants } from './types';
|
|
8
8
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
9
9
|
export { createDragControls } from './utils/dragControls';
|
|
10
10
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
11
11
|
export { useMotionValue } from './utils/motionValue';
|
|
12
12
|
export type { MotionValue } from './utils/motionValue';
|
|
13
13
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
14
|
+
export { useReducedMotion } from './utils/reducedMotion';
|
|
15
|
+
export { useReducedMotionConfig } from './utils/reducedMotionConfig';
|
|
14
16
|
export { useScroll } from './utils/scroll';
|
|
15
17
|
export { useSpring } from './utils/spring';
|
|
16
18
|
export { useVelocity } from './utils/velocity';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export { createDragControls } from './utils/dragControls';
|
|
|
12
12
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
13
13
|
export { useMotionValue } from './utils/motionValue';
|
|
14
14
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
15
|
+
export { useReducedMotion } from './utils/reducedMotion';
|
|
16
|
+
export { useReducedMotionConfig } from './utils/reducedMotionConfig';
|
|
15
17
|
export { useScroll } from './utils/scroll';
|
|
16
18
|
export { useSpring } from './utils/spring';
|
|
17
19
|
export { useVelocity } from './utils/velocity';
|
package/dist/types.d.ts
CHANGED
|
@@ -323,9 +323,27 @@ export type MotionProps = {
|
|
|
323
323
|
* - delay: Time to wait before starting the animation
|
|
324
324
|
* - repeat: Number of times to repeat the animation
|
|
325
325
|
*/
|
|
326
|
+
/**
|
|
327
|
+
* Reduced-motion policy for {@link MotionConfigProps.reducedMotion}.
|
|
328
|
+
*
|
|
329
|
+
* - `'never'` (default): Animations run as authored, regardless of OS preference.
|
|
330
|
+
* - `'always'`: Transform animations (x, y, scale, rotate, skew, translate) are
|
|
331
|
+
* skipped. Other properties such as `opacity` and `color` still animate.
|
|
332
|
+
* - `'user'`: Honors the OS-level `prefers-reduced-motion: reduce` setting —
|
|
333
|
+
* behaves like `'always'` when the user has opted in, otherwise `'never'`.
|
|
334
|
+
*
|
|
335
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
336
|
+
*/
|
|
337
|
+
export type ReducedMotionConfig = 'user' | 'always' | 'never';
|
|
326
338
|
export type MotionConfigProps = {
|
|
327
339
|
/** Animation configuration */
|
|
328
340
|
transition?: MotionTransition;
|
|
341
|
+
/**
|
|
342
|
+
* Reduced-motion policy applied to descendant motion elements.
|
|
343
|
+
*
|
|
344
|
+
* Defaults to `'never'`. See {@link ReducedMotionConfig}.
|
|
345
|
+
*/
|
|
346
|
+
reducedMotion?: ReducedMotionConfig;
|
|
329
347
|
};
|
|
330
348
|
/**
|
|
331
349
|
* AnimatePresence mode controls how enter and exit animations are coordinated.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a readable store that reflects the user's `prefers-reduced-motion` setting.
|
|
4
|
+
*
|
|
5
|
+
* Defaults to `false` in SSR or when `matchMedia` is unavailable/throws.
|
|
6
|
+
*
|
|
7
|
+
* @returns {Readable<boolean>} `true` when the user prefers reduced motion.
|
|
8
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script>
|
|
13
|
+
* import { useReducedMotion } from '@humanspeak/svelte-motion'
|
|
14
|
+
* const reduced = useReducedMotion()
|
|
15
|
+
* </script>
|
|
16
|
+
*
|
|
17
|
+
* <div style:transform={$reduced ? 'none' : 'rotate(45deg)'}>...</div>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const useReducedMotion: () => Readable<boolean>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readable } from 'svelte/store';
|
|
2
|
+
const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
|
3
|
+
const SSR_FALLBACK = readable(false, () => { });
|
|
4
|
+
/**
|
|
5
|
+
* Returns a readable store that reflects the user's `prefers-reduced-motion` setting.
|
|
6
|
+
*
|
|
7
|
+
* Defaults to `false` in SSR or when `matchMedia` is unavailable/throws.
|
|
8
|
+
*
|
|
9
|
+
* @returns {Readable<boolean>} `true` when the user prefers reduced motion.
|
|
10
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <script>
|
|
15
|
+
* import { useReducedMotion } from '@humanspeak/svelte-motion'
|
|
16
|
+
* const reduced = useReducedMotion()
|
|
17
|
+
* </script>
|
|
18
|
+
*
|
|
19
|
+
* <div style:transform={$reduced ? 'none' : 'rotate(45deg)'}>...</div>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const useReducedMotion = () => {
|
|
23
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
24
|
+
return SSR_FALLBACK;
|
|
25
|
+
}
|
|
26
|
+
let media;
|
|
27
|
+
try {
|
|
28
|
+
media = window.matchMedia(REDUCED_MOTION_QUERY);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return SSR_FALLBACK;
|
|
32
|
+
}
|
|
33
|
+
return readable(media.matches, (set) => {
|
|
34
|
+
const handler = (event) => set(event.matches);
|
|
35
|
+
if (typeof media.addEventListener === 'function') {
|
|
36
|
+
media.addEventListener('change', handler);
|
|
37
|
+
return () => media.removeEventListener('change', handler);
|
|
38
|
+
}
|
|
39
|
+
media.addListener(handler);
|
|
40
|
+
return () => media.removeListener(handler);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a copy of `keyframes` with transform-related keys removed when
|
|
4
|
+
* `reduced` is `true`. Returns `keyframes` unchanged otherwise.
|
|
5
|
+
*
|
|
6
|
+
* The `transition` key is preserved so per-key transitions still flow through
|
|
7
|
+
* to the animation engine.
|
|
8
|
+
*/
|
|
9
|
+
export declare function filterReducedMotionKeyframes<T extends Record<string, unknown> | undefined>(keyframes: T, reduced: boolean): T;
|
|
10
|
+
/**
|
|
11
|
+
* Returns a readable store that reflects the resolved reduced-motion policy
|
|
12
|
+
* for the current component subtree.
|
|
13
|
+
*
|
|
14
|
+
* Resolution combines the nearest `<MotionConfig reducedMotion>` ancestor with
|
|
15
|
+
* the OS-level `prefers-reduced-motion` setting:
|
|
16
|
+
*
|
|
17
|
+
* - `'always'` → always `true`
|
|
18
|
+
* - `'never'` (or no `<MotionConfig>` ancestor) → always `false`
|
|
19
|
+
* - `'user'` → mirrors the OS preference, defaulting to `false` in SSR
|
|
20
|
+
*
|
|
21
|
+
* Use this from inside motion-aware components to decide whether to skip
|
|
22
|
+
* transform animations.
|
|
23
|
+
*
|
|
24
|
+
* @returns {Readable<boolean>} `true` when descendant motion should be reduced.
|
|
25
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```svelte
|
|
29
|
+
* <script>
|
|
30
|
+
* import { useReducedMotionConfig } from '@humanspeak/svelte-motion'
|
|
31
|
+
* const reduced = useReducedMotionConfig()
|
|
32
|
+
* </script>
|
|
33
|
+
*
|
|
34
|
+
* {#if !$reduced}
|
|
35
|
+
* <motion.div animate={{ x: 100 }} />
|
|
36
|
+
* {/if}
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare const useReducedMotionConfig: () => Readable<boolean>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getMotionConfig } from '../components/motionConfig.context.js';
|
|
2
|
+
import { useReducedMotion } from './reducedMotion.js';
|
|
3
|
+
import { derived } from 'svelte/store';
|
|
4
|
+
/**
|
|
5
|
+
* CSS / motion property keys that move or rotate an element via `transform`.
|
|
6
|
+
* When reduced motion is active these keys are stripped from animate keyframes
|
|
7
|
+
* so the element stays in place while non-transform properties (opacity, color,
|
|
8
|
+
* etc.) continue to animate.
|
|
9
|
+
*/
|
|
10
|
+
const TRANSFORM_KEYS = new Set([
|
|
11
|
+
'x',
|
|
12
|
+
'y',
|
|
13
|
+
'z',
|
|
14
|
+
'translate',
|
|
15
|
+
'translateX',
|
|
16
|
+
'translateY',
|
|
17
|
+
'translateZ',
|
|
18
|
+
'scale',
|
|
19
|
+
'scaleX',
|
|
20
|
+
'scaleY',
|
|
21
|
+
'scaleZ',
|
|
22
|
+
'rotate',
|
|
23
|
+
'rotateX',
|
|
24
|
+
'rotateY',
|
|
25
|
+
'rotateZ',
|
|
26
|
+
'skew',
|
|
27
|
+
'skewX',
|
|
28
|
+
'skewY',
|
|
29
|
+
'transform',
|
|
30
|
+
'transformPerspective',
|
|
31
|
+
'perspective'
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Returns a copy of `keyframes` with transform-related keys removed when
|
|
35
|
+
* `reduced` is `true`. Returns `keyframes` unchanged otherwise.
|
|
36
|
+
*
|
|
37
|
+
* The `transition` key is preserved so per-key transitions still flow through
|
|
38
|
+
* to the animation engine.
|
|
39
|
+
*/
|
|
40
|
+
export function filterReducedMotionKeyframes(keyframes, reduced) {
|
|
41
|
+
if (!reduced || !keyframes)
|
|
42
|
+
return keyframes;
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const key of Object.keys(keyframes)) {
|
|
45
|
+
if (!TRANSFORM_KEYS.has(key))
|
|
46
|
+
out[key] = keyframes[key];
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns a readable store that reflects the resolved reduced-motion policy
|
|
52
|
+
* for the current component subtree.
|
|
53
|
+
*
|
|
54
|
+
* Resolution combines the nearest `<MotionConfig reducedMotion>` ancestor with
|
|
55
|
+
* the OS-level `prefers-reduced-motion` setting:
|
|
56
|
+
*
|
|
57
|
+
* - `'always'` → always `true`
|
|
58
|
+
* - `'never'` (or no `<MotionConfig>` ancestor) → always `false`
|
|
59
|
+
* - `'user'` → mirrors the OS preference, defaulting to `false` in SSR
|
|
60
|
+
*
|
|
61
|
+
* Use this from inside motion-aware components to decide whether to skip
|
|
62
|
+
* transform animations.
|
|
63
|
+
*
|
|
64
|
+
* @returns {Readable<boolean>} `true` when descendant motion should be reduced.
|
|
65
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```svelte
|
|
69
|
+
* <script>
|
|
70
|
+
* import { useReducedMotionConfig } from '@humanspeak/svelte-motion'
|
|
71
|
+
* const reduced = useReducedMotionConfig()
|
|
72
|
+
* </script>
|
|
73
|
+
*
|
|
74
|
+
* {#if !$reduced}
|
|
75
|
+
* <motion.div animate={{ x: 100 }} />
|
|
76
|
+
* {/if}
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export const useReducedMotionConfig = () => {
|
|
80
|
+
const motionConfig = getMotionConfig();
|
|
81
|
+
// Read motionConfig?.reducedMotion *inside* the derived so dynamic
|
|
82
|
+
// `<MotionConfig reducedMotion={...}>` updates surface to subscribers —
|
|
83
|
+
// motionConfig uses property getters, so the value is always fresh.
|
|
84
|
+
return derived(useReducedMotion(), ($osReduced) => {
|
|
85
|
+
const policy = motionConfig?.reducedMotion ?? 'never';
|
|
86
|
+
if (policy === 'always')
|
|
87
|
+
return true;
|
|
88
|
+
if (policy === 'never')
|
|
89
|
+
return false;
|
|
90
|
+
return $osReduced;
|
|
91
|
+
});
|
|
92
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A Framer Motion-compatible animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|