@humanspeak/svelte-motion 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -30,7 +30,7 @@ All standard HTML and SVG elements are supported as motion components (e.g., `mo
30
30
 
31
31
  ### MotionConfig
32
32
 
33
- This package includes support for `MotionConfig`, which allows you to set default motion settings for all child components. See the [Reach - Motion Config](https://motion.dev/docs/react-motion-config) for more details.
33
+ This package includes support for `MotionConfig`, which allows you to set default motion settings for all child components. See the [React - Motion Config](https://motion.dev/docs/react-motion-config) for more details.
34
34
 
35
35
  ```svelte
36
36
  <MotionConfig transition={{ duration: 0.5 }}>
@@ -111,6 +111,7 @@ This package carefully selects its dependencies to provide a robust and maintain
111
111
  | [HTML Content (0→100 counter)](https://examples.motion.dev/react/html-content) | `/tests/motion/html-content` | [View Example](https://motion.svelte.page/examples/html-content) |
112
112
  | [Aspect Ratio](https://examples.motion.dev/react/aspect-ratio) | `/tests/motion/aspect-ratio` | [View Example](https://svelte.dev/playground/1bf60e745fae44f5becb4c830fde9b6e?version=5.38.10) |
113
113
  | [Hover + Tap (whileHover + whileTap)](https://examples.motion.dev/react/gestures) | `/tests/motion/hover-and-tap` | [View Example](https://motion.svelte.page/examples/hover-and-tap) |
114
+ | [Focus (whileFocus)](https://motion.dev/docs/react-motion-component#focus) | `/tests/motion/while-focus` | [View Example](https://motion.svelte.page/examples/while-focus) |
114
115
  | [Rotate](https://examples.motion.dev/react/rotate) | `/tests/motion/rotate` | [View Example](https://motion.svelte.page/examples/rotate) |
115
116
  | [Random - Shiny Button](https://www.youtube.com/watch?v=jcpLprT5F0I) by [@verse\_](https://x.com/verse_) | `/tests/random/shiny-button` | [View Example](https://svelte.dev/playground/96f9e0bf624f4396adaf06c519147450?version=5.38.10) |
116
117
  | [Fancy Like Button](https://github.com/DRlFTER/fancyLikeButton) | `/tests/random/fancy-like-button` | [View Example](https://svelte.dev/playground/c34b7e53d41c48b0ab1eaf21ca120c6e?version=5.38.10) |
@@ -153,6 +154,16 @@ Svelte Motion now supports hover interactions via the `whileHover` prop, similar
153
154
  - Blur while key is held → fires `onTapCancel`
154
155
  - `MotionContainer` sets `tabindex="0"` automatically when `whileTap` is present and no `tabindex`/`tabIndex` is provided.
155
156
 
157
+ ### Focus
158
+
159
+ ```svelte
160
+ <motion.button whileFocus={{ scale: 1.05, outline: '2px solid blue' }} />
161
+ ```
162
+
163
+ - Animates when the element receives keyboard focus and restores baseline on blur.
164
+ - Callbacks: `onFocusStart`, `onFocusEnd` are supported.
165
+ - Perfect for keyboard navigation and accessibility enhancements.
166
+
156
167
  ### Animation lifecycle
157
168
 
158
169
  ```svelte
@@ -166,6 +177,77 @@ Svelte Motion now supports hover interactions via the `whileHover` prop, similar
166
177
  />
167
178
  ```
168
179
 
180
+ ## Variants
181
+
182
+ Variants allow you to define named animation states that can be referenced throughout your component tree. They're perfect for creating reusable animations and orchestrating complex sequences.
183
+
184
+ ### Basic usage
185
+
186
+ Instead of defining animation objects inline, create a `Variants` object with named states:
187
+
188
+ ```svelte
189
+ <script lang="ts">
190
+ import { motion, type Variants } from '@humanspeak/svelte-motion'
191
+
192
+ let isOpen = $state(false)
193
+
194
+ const variants: Variants = {
195
+ open: { opacity: 1, scale: 1 },
196
+ closed: { opacity: 0, scale: 0.8 }
197
+ }
198
+ </script>
199
+
200
+ <motion.div {variants} initial="closed" animate={isOpen ? 'open' : 'closed'}>Click me</motion.div>
201
+ ```
202
+
203
+ ### Variant propagation
204
+
205
+ One of the most powerful features is **automatic propagation** through component trees. When a parent changes its animation state, all children with `variants` defined automatically inherit that state:
206
+
207
+ ```svelte
208
+ <script lang="ts">
209
+ let isVisible = $state(false)
210
+
211
+ const containerVariants: Variants = {
212
+ visible: { opacity: 1 },
213
+ hidden: { opacity: 0 }
214
+ }
215
+
216
+ const itemVariants: Variants = {
217
+ visible: { opacity: 1, x: 0 },
218
+ hidden: { opacity: 0, x: -20 }
219
+ }
220
+ </script>
221
+
222
+ <motion.ul variants={containerVariants} initial="hidden" animate={isVisible ? 'visible' : 'hidden'}>
223
+ <!-- Children automatically inherit parent's variant state -->
224
+ <motion.li variants={itemVariants}>Item 1</motion.li>
225
+ <motion.li variants={itemVariants}>Item 2</motion.li>
226
+ <motion.li variants={itemVariants}>Item 3</motion.li>
227
+ </motion.ul>
228
+ ```
229
+
230
+ **How it works:**
231
+
232
+ - Parent sets `animate="visible"`
233
+ - Children with `variants` automatically inherit `"visible"` state
234
+ - Each child resolves its own variant definition
235
+ - No need to pass `animate` props to children!
236
+
237
+ ### Staggered animations
238
+
239
+ Create staggered animations with transition delays:
240
+
241
+ ```svelte
242
+ {#each items as item, i}
243
+ <motion.div variants={itemVariants} transition={{ delay: i * 0.1 }}>
244
+ {item}
245
+ </motion.div>
246
+ {/each}
247
+ ```
248
+
249
+ See the [Variants documentation](https://motion.svelte.page/docs/variants) for complete details and examples.
250
+
169
251
  ## Server-side rendering
170
252
 
171
253
  Motion components render their initial state during SSR. The container merges inline `style` with the first values from `initial` (or the first keyframes from `animate` when `initial` is empty) so the server HTML matches the starting appearance. On hydration, components promote to a ready state and animate without flicker.
@@ -204,6 +286,36 @@ Notes:
204
286
  <motion.div style={`rotate: ${$rotate}deg`} />
205
287
  ```
206
288
 
289
+ ### useAnimationFrame(callback)
290
+
291
+ - Runs a callback on every animation frame with the current timestamp.
292
+ - The callback receives a `DOMHighResTimeStamp` representing the time elapsed since the time origin.
293
+ - Returns a cleanup function that stops the animation loop.
294
+ - Best used inside a `$effect` to ensure proper cleanup when the component unmounts.
295
+ - SSR-safe: Does nothing and returns a no-op cleanup function when `window` is unavailable.
296
+
297
+ ```svelte
298
+ <script lang="ts">
299
+ import { useAnimationFrame } from '$lib'
300
+
301
+ let cubeRef: HTMLDivElement
302
+
303
+ $effect(() => {
304
+ return useAnimationFrame((t) => {
305
+ if (!cubeRef) return
306
+
307
+ const rotate = Math.sin(t / 10000) * 200
308
+ const y = (1 + Math.sin(t / 1000)) * -50
309
+ cubeRef.style.transform = `translateY(${y}px) rotateX(${rotate}deg) rotateY(${rotate}deg)`
310
+ })
311
+ })
312
+ </script>
313
+
314
+ <div bind:this={cubeRef}>Animated content</div>
315
+ ```
316
+
317
+ - Reference: Motion useAnimationFrame docs [motion.dev](https://motion.dev/docs/react-use-animation-frame).
318
+
207
319
  ### useSpring
208
320
 
209
321
  `useSpring` creates a readable store that animates to its latest target with a spring. You can either control it directly with `set`/`jump`, or have it follow another readable (like a time-derived value).
@@ -0,0 +1,19 @@
1
+ import type { Writable } from 'svelte/store';
2
+ /**
3
+ * Provide a writable store for the current variant key so children can
4
+ * react to changes over time (true inheritance like Framer Motion).
5
+ */
6
+ export declare function setVariantContext(store: Writable<string | undefined>): void;
7
+ /**
8
+ * Read the parent's variant store (if any). Children subscribe to this store
9
+ * to inherit and react to parent `animate` changes.
10
+ */
11
+ export declare function getVariantContext(): Writable<string | undefined> | undefined;
12
+ /**
13
+ * Set initial={false} in context so children inherit it
14
+ */
15
+ export declare function setInitialFalseContext(value: boolean): void;
16
+ /**
17
+ * Check if parent has initial={false}
18
+ */
19
+ export declare function getInitialFalseContext(): boolean;
@@ -0,0 +1,29 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const VARIANT_CONTEXT_KEY = Symbol('variant-context');
3
+ const INITIAL_FALSE_CONTEXT_KEY = Symbol('initial-false-context');
4
+ /**
5
+ * Provide a writable store for the current variant key so children can
6
+ * react to changes over time (true inheritance like Framer Motion).
7
+ */
8
+ export function setVariantContext(store) {
9
+ setContext(VARIANT_CONTEXT_KEY, store);
10
+ }
11
+ /**
12
+ * Read the parent's variant store (if any). Children subscribe to this store
13
+ * to inherit and react to parent `animate` changes.
14
+ */
15
+ export function getVariantContext() {
16
+ return getContext(VARIANT_CONTEXT_KEY);
17
+ }
18
+ /**
19
+ * Set initial={false} in context so children inherit it
20
+ */
21
+ export function setInitialFalseContext(value) {
22
+ setContext(INITIAL_FALSE_CONTEXT_KEY, value);
23
+ }
24
+ /**
25
+ * Check if parent has initial={false}
26
+ */
27
+ export function getInitialFalseContext() {
28
+ return getContext(INITIAL_FALSE_CONTEXT_KEY) ?? false;
29
+ }
@@ -9,6 +9,7 @@
9
9
  import { mergeTransitions, animateWithLifecycle } from '../utils/animation'
10
10
  import { attachWhileTap } from '../utils/interaction'
11
11
  import { attachWhileHover } from '../utils/hover'
12
+ import { attachWhileFocus } from '../utils/focus'
12
13
  import {
13
14
  measureRect,
14
15
  computeFlipTransforms,
@@ -21,6 +22,14 @@
21
22
  import { isNativelyFocusable } from '../utils/a11y'
22
23
  import { usePresence, getAnimatePresenceContext } from '../utils/presence'
23
24
  import { getInitialKeyframes } from '../utils/initial'
25
+ import { resolveInitial, resolveAnimate, resolveExit } from '../utils/variants'
26
+ import {
27
+ setVariantContext,
28
+ getVariantContext,
29
+ setInitialFalseContext,
30
+ getInitialFalseContext
31
+ } from '../components/variantContext.context'
32
+ import { writable } from 'svelte/store'
24
33
 
25
34
  type Props = MotionProps & {
26
35
  children?: Snippet
@@ -31,6 +40,7 @@
31
40
  let {
32
41
  children,
33
42
  tag = 'div',
43
+ variants: variantsProp,
34
44
  initial: initialProp,
35
45
  animate: animateProp,
36
46
  exit: exitProp,
@@ -41,8 +51,11 @@
41
51
  class: classProp,
42
52
  whileTap: whileTapProp,
43
53
  whileHover: whileHoverProp,
54
+ whileFocus: whileFocusProp,
44
55
  onHoverStart: onHoverStartProp,
45
56
  onHoverEnd: onHoverEndProp,
57
+ onFocusStart: onFocusStartProp,
58
+ onFocusEnd: onFocusEndProp,
46
59
  onTapStart: onTapStartProp,
47
60
  onTap: onTapProp,
48
61
  onTapCancel: onTapCancelProp,
@@ -71,7 +84,7 @@
71
84
  usePresence(
72
85
  presenceKey,
73
86
  element,
74
- exitProp,
87
+ resolvedExit,
75
88
  mergedTransition as unknown as MotionTransition
76
89
  )
77
90
  }
@@ -140,8 +153,67 @@
140
153
  // Recognized HTML void elements that cannot contain children
141
154
  const isVoidTag = $derived(VOID_TAGS.has(tag as string))
142
155
 
143
- // Extract keyframes from initialProp, handling initial={false}
144
- const initialKeyframes = $derived(getInitialKeyframes(initialProp))
156
+ // Variant inheritance and resolution
157
+ const parentVariantStore = getVariantContext()
158
+
159
+ // Get initial inherited variant synchronously
160
+ let initialInheritedVariant: string | undefined = undefined
161
+ if (parentVariantStore) {
162
+ parentVariantStore.subscribe((v) => (initialInheritedVariant = v))()
163
+ }
164
+
165
+ // Create store with initial value so children can inherit immediately
166
+ const initialVariantValue =
167
+ typeof animateProp === 'string'
168
+ ? animateProp
169
+ : (variantsProp && initialInheritedVariant) || undefined
170
+ const localVariantStore = writable<string | undefined>(initialVariantValue)
171
+
172
+ let inheritedVariant = $state<string | undefined>(initialInheritedVariant)
173
+
174
+ $effect(() => {
175
+ if (!parentVariantStore) {
176
+ inheritedVariant = undefined
177
+ return
178
+ }
179
+ const unsubscribe = parentVariantStore.subscribe((v) => (inheritedVariant = v))
180
+ return () => unsubscribe()
181
+ })
182
+
183
+ // Use the initial value first, then switch to reactive once mounted
184
+ const effectiveAnimate = $derived(
185
+ animateProp ?? (variantsProp ? (inheritedVariant ?? initialInheritedVariant) : undefined)
186
+ )
187
+
188
+ // Propagate initial={false} to children BEFORE setting variant context
189
+ const parentInitialFalse = getInitialFalseContext()
190
+ const effectiveInitialProp =
191
+ initialProp !== undefined
192
+ ? initialProp
193
+ : parentInitialFalse && variantsProp
194
+ ? false
195
+ : undefined
196
+
197
+ if (initialProp === false) {
198
+ setInitialFalseContext(true)
199
+ }
200
+
201
+ // Provide context immediately during initialization so children can inherit
202
+ setVariantContext(localVariantStore)
203
+
204
+ $effect(() => {
205
+ if (!variantsProp) return localVariantStore.set(undefined)
206
+ if (typeof animateProp === 'string') return localVariantStore.set(animateProp)
207
+ if (typeof effectiveAnimate === 'string') return localVariantStore.set(effectiveAnimate)
208
+ localVariantStore.set(undefined)
209
+ })
210
+
211
+ const resolvedInitial = $derived(resolveInitial(effectiveInitialProp, variantsProp))
212
+ const resolvedAnimate = $derived(resolveAnimate(effectiveAnimate, variantsProp))
213
+ const resolvedExit = $derived(resolveExit(exitProp, variantsProp))
214
+
215
+ // Extract keyframes from resolved initial, handling initial={false}
216
+ const initialKeyframes = $derived(getInitialKeyframes(resolvedInitial))
145
217
 
146
218
  // Derived attributes to keep both branches in sync (focusability, data flags, style, class)
147
219
  const derivedAttrs = $derived<Record<string, unknown>>({
@@ -163,15 +235,15 @@
163
235
  style: mergeInlineStyles(
164
236
  styleProp,
165
237
  initialKeyframes as unknown as Record<string, unknown>,
166
- animateProp as unknown as Record<string, unknown>
238
+ resolvedAnimate as unknown as Record<string, unknown>
167
239
  ),
168
240
  class: classProp
169
241
  })
170
242
 
171
243
  const runAnimation = () => {
172
- if (!element || !animateProp) return
244
+ if (!element || !resolvedAnimate) return
173
245
  const transitionAnimate: MotionTransition = mergedTransition ?? {}
174
- const payload = $state.snapshot(animateProp)
246
+ const payload = $state.snapshot(resolvedAnimate)
175
247
  animateWithLifecycle(
176
248
  element,
177
249
  payload as unknown as DOMKeyframesDefinition,
@@ -181,6 +253,17 @@
181
253
  )
182
254
  }
183
255
 
256
+ // Track the last variant key we ran to avoid re-running on mount
257
+ let lastRanVariantKey = $state<string | undefined>(undefined)
258
+ let mountedWithInitialFalse = $state(false)
259
+ const currentAnimateKey = $derived(
260
+ typeof animateProp === 'string'
261
+ ? animateProp
262
+ : typeof effectiveAnimate === 'string'
263
+ ? effectiveAnimate
264
+ : undefined
265
+ )
266
+
184
267
  // Minimal layout animation using FLIP when `layout` is enabled.
185
268
  // When layout === 'position' we only translate.
186
269
  // When layout === true we also scale to smoothly interpolate size changes.
@@ -231,8 +314,8 @@
231
314
  return attachWhileTap(
232
315
  element!,
233
316
  (whileTapProp ?? {}) as Record<string, unknown>,
234
- (initialProp ?? {}) as Record<string, unknown>,
235
- (animateProp ?? {}) as Record<string, unknown>,
317
+ (resolvedInitial ?? {}) as Record<string, unknown>,
318
+ (resolvedAnimate ?? {}) as Record<string, unknown>,
236
319
  {
237
320
  onTapStart: onTapStartProp,
238
321
  onTap: onTapProp,
@@ -253,23 +336,93 @@
253
336
  { onStart: onHoverStartProp, onEnd: onHoverEndProp },
254
337
  undefined,
255
338
  {
256
- initial: (initialProp ?? {}) as Record<string, unknown>,
257
- animate: (animateProp ?? {}) as Record<string, unknown>
339
+ initial: (resolvedInitial ?? {}) as Record<string, unknown>,
340
+ animate: (resolvedAnimate ?? {}) as Record<string, unknown>
341
+ }
342
+ )
343
+ })
344
+
345
+ // whileFocus handling for keyboard focus interactions
346
+ $effect(() => {
347
+ if (!(element && isLoaded === 'ready' && isNotEmpty(whileFocusProp))) return
348
+ return attachWhileFocus(
349
+ element!,
350
+ (whileFocusProp ?? {}) as Record<string, unknown>,
351
+ (mergedTransition ?? {}) as AnimationOptions,
352
+ { onStart: onFocusStartProp, onEnd: onFocusEndProp },
353
+ {
354
+ initial: (resolvedInitial ?? {}) as Record<string, unknown>,
355
+ animate: (resolvedAnimate ?? {}) as Record<string, unknown>
258
356
  }
259
357
  )
260
358
  })
261
359
 
262
360
  // Re-run animate when animateProp changes while ready
263
361
  $effect(() => {
264
- if (element && isLoaded === 'ready' && isNotEmpty(animateProp)) {
362
+ if (!(element && isLoaded === 'ready')) return
363
+ // Skip first run if we mounted with initial={false} AND the variant hasn't changed
364
+ if (mountedWithInitialFalse) {
365
+ // Only skip if the variant is the same as what we mounted with
366
+ if (typeof animateProp === 'string' && lastRanVariantKey === animateProp) {
367
+ mountedWithInitialFalse = false
368
+ return
369
+ }
370
+ // Variant has changed, so we should animate
371
+ mountedWithInitialFalse = false
372
+ }
373
+ if (typeof animateProp === 'string') {
374
+ if (lastRanVariantKey !== animateProp) {
375
+ lastRanVariantKey = animateProp
376
+ runAnimation()
377
+ }
378
+ } else if (animateProp) {
379
+ // Object animate props - always run
380
+ lastRanVariantKey = undefined
381
+ runAnimation()
382
+ }
383
+ })
384
+
385
+ // Also run when inherited/effective variant changes
386
+ $effect(() => {
387
+ void resolvedAnimate
388
+ if (!(element && isLoaded === 'ready' && !animateProp && resolvedAnimate)) return
389
+ // Skip first run if we mounted with initial={false} AND the variant hasn't changed
390
+ if (mountedWithInitialFalse) {
391
+ // Only skip if the variant is the same as what we mounted with
392
+ if (typeof currentAnimateKey === 'string' && lastRanVariantKey === currentAnimateKey) {
393
+ mountedWithInitialFalse = false
394
+ return
395
+ }
396
+ // Variant has changed, so we should animate
397
+ mountedWithInitialFalse = false
398
+ }
399
+ if (typeof currentAnimateKey === 'string') {
400
+ if (lastRanVariantKey !== currentAnimateKey) {
401
+ lastRanVariantKey = currentAnimateKey
402
+ runAnimation()
403
+ }
404
+ } else {
265
405
  runAnimation()
266
406
  }
267
407
  })
268
408
 
269
409
  $effect(() => {
270
410
  if (!(element && isLoaded === 'mounting')) return
271
- if (animateProp) {
272
- if (isNotEmpty(initialKeyframes)) {
411
+ if (effectiveAnimate) {
412
+ // If initial={false}, render at animate state immediately with no transition
413
+ if (effectiveInitialProp === false && resolvedAnimate) {
414
+ // Use Motion's animate() with duration:0 so it takes control of these properties
415
+ // This prevents inline styles from pinning the properties during future animations
416
+ const snapshot = $state.snapshot(resolvedAnimate) as Record<string, unknown>
417
+ animate(element!, snapshot as DOMKeyframesDefinition, { duration: 0 })
418
+ // Mark that we've already applied this variant to avoid a second animate pass
419
+ mountedWithInitialFalse = true
420
+ if (typeof currentAnimateKey === 'string') {
421
+ lastRanVariantKey = currentAnimateKey
422
+ }
423
+ dataPath = 5
424
+ isLoaded = 'ready'
425
+ } else if (isNotEmpty(initialKeyframes)) {
273
426
  // Apply initial instantly BEFORE exposing 'initial' state
274
427
  animate(element!, initialKeyframes!, { duration: 0 })
275
428
  // Mark initial after styles are applied so tests read CSS=0 while state=initial
@@ -286,7 +439,20 @@
286
439
  } else {
287
440
  dataPath = 2
288
441
  isLoaded = 'ready'
289
- runAnimation()
442
+ // If we're inheriting a variant and parent had initial={false}, apply the variant instantly
443
+ // without animation, then mark it as applied
444
+ if (
445
+ parentInitialFalse &&
446
+ typeof currentAnimateKey === 'string' &&
447
+ resolvedAnimate
448
+ ) {
449
+ // Apply variant styles instantly with duration:0
450
+ const snapshot = $state.snapshot(resolvedAnimate) as Record<string, unknown>
451
+ animate(element!, snapshot as DOMKeyframesDefinition, { duration: 0 })
452
+ lastRanVariantKey = currentAnimateKey
453
+ } else {
454
+ runAnimation()
455
+ }
290
456
  }
291
457
  } else if (isNotEmpty(initialKeyframes)) {
292
458
  // Apply initial instantly BEFORE exposing 'initial' state
package/dist/index.d.ts CHANGED
@@ -3,8 +3,10 @@ import MotionConfig from './components/MotionConfig.svelte';
3
3
  import type { MotionComponents } from './html/index';
4
4
  export declare const motion: MotionComponents;
5
5
  export { animate, hover } from 'motion';
6
- export type { MotionAnimate, MotionInitial, MotionTransition, MotionWhileTap } from './types';
6
+ export type { MotionAnimate, MotionInitial, MotionTransition, MotionWhileFocus, MotionWhileHover, MotionWhileTap, Variants } from './types';
7
+ export { useAnimationFrame } from './utils/animationFrame';
7
8
  export { useSpring } from './utils/spring';
9
+ export { stringifyStyleObject } from './utils/styleObject';
8
10
  export { useTime } from './utils/time';
9
11
  export { useTransform } from './utils/transform';
10
12
  export { AnimatePresence, MotionConfig };
package/dist/index.js CHANGED
@@ -5,7 +5,9 @@ import * as html from './html/index';
5
5
  export const motion = Object.fromEntries(Object.entries(html).map(([key, component]) => [key.toLowerCase(), component]));
6
6
  // Export all types
7
7
  export { animate, hover } from 'motion';
8
+ export { useAnimationFrame } from './utils/animationFrame';
8
9
  export { useSpring } from './utils/spring';
10
+ export { stringifyStyleObject } from './utils/styleObject';
9
11
  export { useTime } from './utils/time';
10
12
  export { useTransform } from './utils/transform';
11
13
  export { AnimatePresence, MotionConfig };
package/dist/types.d.ts CHANGED
@@ -1,10 +1,27 @@
1
1
  import type { AnimationOptions, DOMKeyframesDefinition } from 'motion';
2
2
  import type { Snippet } from 'svelte';
3
+ /**
4
+ * Variants define named animation states that can be referenced by string keys.
5
+ *
6
+ * @example
7
+ * ```svelte
8
+ * <script>
9
+ * const variants = {
10
+ * open: { opacity: 1, scale: 1 },
11
+ * closed: { opacity: 0, scale: 0.8 }
12
+ * }
13
+ * </script>
14
+ *
15
+ * <motion.div variants={variants} animate="open" />
16
+ * ```
17
+ */
18
+ export type Variants = Record<string, DOMKeyframesDefinition | undefined>;
3
19
  /**
4
20
  * Initial animation properties for a motion component.
5
21
  *
6
- * Set to `false` to skip the initial animation and render directly at the animated state.
7
- * Useful for elements that should not animate on mount (e.g., tab indicators).
22
+ * - Can be an object with animation properties
23
+ * - Can be a string key referencing a variant
24
+ * - Set to `false` to skip the initial animation and render directly at the animated state
8
25
  *
9
26
  * @example
10
27
  * ```svelte
@@ -13,27 +30,44 @@ import type { Snippet } from 'svelte';
13
30
  *
14
31
  * <!-- Skip initial animation, render at animate state -->
15
32
  * <motion.div initial={false} animate={{ opacity: 1 }} />
33
+ *
34
+ * <!-- Use variant key -->
35
+ * <motion.div variants={myVariants} initial="hidden" animate="visible" />
16
36
  * ```
17
37
  */
18
- export type MotionInitial = DOMKeyframesDefinition | false | undefined;
38
+ export type MotionInitial = DOMKeyframesDefinition | string | false | undefined;
19
39
  /**
20
40
  * Target animation properties for a motion component.
41
+ *
42
+ * - Can be an object with animation properties
43
+ * - Can be a string key referencing a variant
44
+ *
21
45
  * @example
22
46
  * ```svelte
23
47
  * <motion.div animate={{ opacity: 1, scale: 1 }} />
48
+ *
49
+ * <!-- With variants -->
50
+ * <motion.div variants={myVariants} animate="visible" />
24
51
  * ```
25
52
  */
26
- export type MotionAnimate = DOMKeyframesDefinition | undefined;
53
+ export type MotionAnimate = DOMKeyframesDefinition | string | undefined;
27
54
  /**
28
55
  * Exit animation properties for a motion component when unmounted.
56
+ *
57
+ * - Can be an object with animation properties
58
+ * - Can be a string key referencing a variant
59
+ *
29
60
  * @example
30
61
  * ```svelte
31
62
  * <motion.div exit={{ opacity: 0, scale: 0 }} />
63
+ *
64
+ * <!-- With variants -->
65
+ * <motion.div variants={myVariants} exit="hidden" />
32
66
  * ```
33
67
  */
34
68
  export type MotionExit = (Record<string, unknown> & {
35
69
  transition?: AnimationOptions;
36
- }) | DOMKeyframesDefinition | undefined;
70
+ }) | DOMKeyframesDefinition | string | undefined;
37
71
  /**
38
72
  * Animation transition configuration.
39
73
  * @example
@@ -69,6 +103,16 @@ export type MotionWhileTap = DOMKeyframesDefinition | undefined;
69
103
  export type MotionWhileHover = (Record<string, unknown> & {
70
104
  transition?: AnimationOptions;
71
105
  }) | DOMKeyframesDefinition | undefined;
106
+ /**
107
+ * Animation properties for focus interactions.
108
+ * @example
109
+ * ```svelte
110
+ * <motion.button whileFocus={{ scale: 1.05 }} />
111
+ * ```
112
+ */
113
+ export type MotionWhileFocus = (Record<string, unknown> & {
114
+ transition?: AnimationOptions;
115
+ }) | DOMKeyframesDefinition | undefined;
72
116
  /**
73
117
  * Animation transition configuration for hover interactions.
74
118
  * Overrides the global transition when provided.
@@ -81,6 +125,9 @@ export type MotionAnimationComplete = ((_definition: DOMKeyframesDefinition | un
81
125
  /** Hover lifecycle callbacks */
82
126
  export type MotionOnHoverStart = (() => void) | undefined;
83
127
  export type MotionOnHoverEnd = (() => void) | undefined;
128
+ /** Focus lifecycle callbacks */
129
+ export type MotionOnFocusStart = (() => void) | undefined;
130
+ export type MotionOnFocusEnd = (() => void) | undefined;
84
131
  /** Tap lifecycle callbacks */
85
132
  export type MotionOnTapStart = (() => void) | undefined;
86
133
  export type MotionOnTap = (() => void) | undefined;
@@ -89,11 +136,13 @@ export type MotionOnTapCancel = (() => void) | undefined;
89
136
  * Base motion props shared by all motion components.
90
137
  */
91
138
  export type MotionProps = {
92
- /** Initial state of the animation */
139
+ /** Variants define named animation states */
140
+ variants?: Variants;
141
+ /** Initial state of the animation (object or variant key) */
93
142
  initial?: MotionInitial;
94
- /** Target state of the animation */
143
+ /** Target state of the animation (object or variant key) */
95
144
  animate?: MotionAnimate;
96
- /** Exit animation state when component is removed */
145
+ /** Exit animation state when component is removed (object or variant key) */
97
146
  exit?: MotionExit;
98
147
  /** Animation configuration */
99
148
  transition?: MotionTransition;
@@ -101,6 +150,8 @@ export type MotionProps = {
101
150
  whileTap?: MotionWhileTap;
102
151
  /** Hover interaction animation */
103
152
  whileHover?: MotionWhileHover;
153
+ /** Focus interaction animation */
154
+ whileFocus?: MotionWhileFocus;
104
155
  /** Called right before a main animate transition starts */
105
156
  onAnimationStart?: MotionAnimationStart;
106
157
  /** Called after a main animate transition completes */
@@ -109,6 +160,10 @@ export type MotionProps = {
109
160
  onHoverStart?: MotionOnHoverStart;
110
161
  /** Called when a true hover gesture ends */
111
162
  onHoverEnd?: MotionOnHoverEnd;
163
+ /** Called when element receives keyboard focus */
164
+ onFocusStart?: MotionOnFocusStart;
165
+ /** Called when element loses keyboard focus */
166
+ onFocusEnd?: MotionOnFocusEnd;
112
167
  /** Called when a tap gesture starts (pointerdown recognized) */
113
168
  onTapStart?: MotionOnTapStart;
114
169
  /** Called when a tap gesture ends successfully (pointerup) */
@@ -1,2 +1,22 @@
1
1
  import type { SvelteHTMLElements } from 'svelte/elements';
2
+ /**
3
+ * Determines if an HTML element is natively focusable.
4
+ *
5
+ * Checks whether a given tag with provided attributes can receive keyboard
6
+ * focus without needing an explicit `tabindex`. Elements are considered
7
+ * natively focusable if they have `tabindex`, `tabIndex`, `contenteditable`,
8
+ * or are inherently focusable tags like `button`, `input`, or anchors with `href`.
9
+ *
10
+ * @param tag - The HTML element tag name.
11
+ * @param attrs - Attributes object that may contain focusability hints.
12
+ * @returns `true` if the element is natively focusable, otherwise `false`.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * isNativelyFocusable('button', {}) // true
17
+ * isNativelyFocusable('div', { tabindex: '0' }) // true
18
+ * isNativelyFocusable('a', { href: '/home' }) // true
19
+ * isNativelyFocusable('div', {}) // false
20
+ * ```
21
+ */
2
22
  export declare const isNativelyFocusable: (tag: keyof SvelteHTMLElements, attrs?: Record<string, unknown>) => boolean;