` component using a motion preset on a `` element:
10 |
11 | ```vue
12 |
13 | Text in Motion!
14 |
15 | ```
16 |
17 |
18 |
19 | ## ``
20 |
21 | The `` can be used to apply motion configuration to all of its child elements, this component is renderless by default and can be used as a wrapper by passing a value to the `:is` prop.
22 |
23 | ```vue
24 |
25 |
26 |
27 | Product 1
28 | Description text
29 |
30 |
31 | Product 2
32 | Description text
33 |
34 |
35 | Product 3
36 | Description text
37 |
38 |
39 |
40 | ```
41 |
42 |
43 |
44 |
45 | ## Props
46 |
47 | The `` and `` components allow you to define animation properties (variants) as props.
48 |
49 | - **`is`**: What element should rendered (`div` by default for ``).
50 | - **`preset`**: Motion preset to use (expects camel-case string), see [Presets](/features/presets).
51 |
52 | ### Variant props
53 |
54 | - **`initial`**: Properties the element will have before it is mounted.
55 | - **`enter`**: Properties the element will have after it is mounted.
56 | - **`visible`**: Properties the element will have whenever it is within view. Once out of view, the `initial` properties are reapplied.
57 | - **`visible-once`**: Properties the element will have once it enters the view.
58 | - **`hovered`**: Properties the element will have when hovered.
59 | - **`focused`**: Properties the element will have when it receives focus.
60 | - **`tapped`**: Properties the element will have upon being clicked or tapped.
61 |
62 | Variants can be passed as an object using the `:variants` prop.
63 |
64 | The `:variants` prop combines with other variant properties, allowing for the definition of custom variants from this object.
65 |
66 | Additional variant properties can be explored on the [Variants](/features/variants) page.
67 |
68 | ### Shorthand Props
69 |
70 | We support shorthand props for quickly setting transition properties:
71 |
72 | - **`delay`**
73 | - **`duration`**
74 |
75 | These properties apply to `visible`, `visible-once`, or `enter` variants if specified; otherwise, they default to the `initial` variant.
76 |
77 | ```vue
78 |
79 |
87 | Content to animate!
88 |
89 |
90 | ```
91 |
92 |
--------------------------------------------------------------------------------
/docs/content/2.features/6.motion-instance.md:
--------------------------------------------------------------------------------
1 | # Motion Instance
2 |
3 | Motion instance is the object exposed when binding to a target element using [v-motion](/features/directive-usage) or [useMotion](/features/composable-usage).
4 |
5 | It is composed of three properties, allowing you to interact with the element.
6 |
7 | ## Variant
8 |
9 | The variant is a string reference, that you can modify and watch.
10 |
11 | It represents the current variant name of the element.
12 |
13 | By modifying this variant, you will trigger a transition between the current variant and the one you just set.
14 |
15 | ```vue
16 |
42 | ```
43 |
44 | ##### _Call customEvent to enable the custom variant_ ☝️
45 |
46 | ## Apply
47 |
48 | Apply is a function that lets you animate to a variant definition, without changing the current variant.
49 |
50 | This is useful when used with event listeners, or any temporary modification to the motion properties of the element.
51 |
52 | This is also useful for orchestration, as apply returns a promise, you can await it and chain variant applying.
53 |
54 | Apply accepts both a [Variant Declaration](/features/variants) or a key from the motion instance variants.
55 |
56 | ```vue
57 |
87 | ```
88 |
89 | ##### _Call customEvent to Zboing the element_ ☝️
90 |
91 | ## Stop
92 |
93 | Stop is a function that lets you stop ongoing animations for a specific element.
94 |
95 | Calling it without argument will be stopping all the animations.
96 |
97 | Calling it with an array of [Motion Properties](/features/motion-properties) keys will stop every specified key.
98 |
99 | Calling it with a single motion property key will stop the specified key.
100 |
101 | ```vue
102 |
122 | ```
123 |
124 | ##### _Call customEvent to stop the animations_ ☝️
125 |
--------------------------------------------------------------------------------
/src/utils/style.ts:
--------------------------------------------------------------------------------
1 | import type { ValueType } from 'style-value-types'
2 | import { alpha, color, complex, degrees, filter, number, progressPercentage, px, scale } from 'style-value-types'
3 |
4 | type ValueTypeMap = Record
5 |
6 | /**
7 | * ValueType for "auto"
8 | */
9 | export const auto: ValueType = {
10 | test: (v: any) => v === 'auto',
11 | parse: v => v,
12 | }
13 |
14 | /**
15 | * ValueType for ints
16 | */
17 | const int = {
18 | ...number,
19 | transform: Math.round,
20 | }
21 |
22 | export const valueTypes: ValueTypeMap = {
23 | // Color props
24 | color,
25 | backgroundColor: color,
26 | outlineColor: color,
27 | fill: color,
28 | stroke: color,
29 |
30 | // Border props
31 | borderColor: color,
32 | borderTopColor: color,
33 | borderRightColor: color,
34 | borderBottomColor: color,
35 | borderLeftColor: color,
36 | borderWidth: px,
37 | borderTopWidth: px,
38 | borderRightWidth: px,
39 | borderBottomWidth: px,
40 | borderLeftWidth: px,
41 | borderRadius: px,
42 | radius: px,
43 | borderTopLeftRadius: px,
44 | borderTopRightRadius: px,
45 | borderBottomRightRadius: px,
46 | borderBottomLeftRadius: px,
47 |
48 | // Positioning props
49 | width: px,
50 | maxWidth: px,
51 | height: px,
52 | maxHeight: px,
53 | size: px,
54 | top: px,
55 | right: px,
56 | bottom: px,
57 | left: px,
58 |
59 | // Spacing props
60 | padding: px,
61 | paddingTop: px,
62 | paddingRight: px,
63 | paddingBottom: px,
64 | paddingLeft: px,
65 | margin: px,
66 | marginTop: px,
67 | marginRight: px,
68 | marginBottom: px,
69 | marginLeft: px,
70 |
71 | // Transform props
72 | rotate: degrees,
73 | rotateX: degrees,
74 | rotateY: degrees,
75 | rotateZ: degrees,
76 | scale,
77 | scaleX: scale,
78 | scaleY: scale,
79 | scaleZ: scale,
80 | skew: degrees,
81 | skewX: degrees,
82 | skewY: degrees,
83 | distance: px,
84 | translateX: px,
85 | translateY: px,
86 | translateZ: px,
87 | x: px,
88 | y: px,
89 | z: px,
90 | perspective: px,
91 | transformPerspective: px,
92 | opacity: alpha,
93 | originX: progressPercentage,
94 | originY: progressPercentage,
95 | originZ: px,
96 |
97 | // Misc
98 | zIndex: int,
99 | filter,
100 | WebkitFilter: filter,
101 |
102 | // SVG
103 | fillOpacity: alpha,
104 | strokeOpacity: alpha,
105 | numOctaves: int,
106 | }
107 |
108 | /**
109 | * Return the value type for a key.
110 | *
111 | * @param key
112 | */
113 | export const getValueType = (key: string) => valueTypes[key]
114 |
115 | /**
116 | * Transform the value using its value type if value is a `number`, otherwise return the value.
117 | *
118 | * @param value
119 | * @param type
120 | */
121 | export function getValueAsType(value: any, type?: ValueType) {
122 | return type && typeof value === 'number' && type.transform ? type.transform(value) : value
123 | }
124 |
125 | /**
126 | * Get default animatable
127 | *
128 | * @param key
129 | * @param value
130 | */
131 | export function getAnimatableNone(key: string, value: string): any {
132 | let defaultValueType = getValueType(key)
133 | if (defaultValueType !== filter)
134 | defaultValueType = complex
135 | // If value is not recognised as animatable, ie "none", create an animatable version origin based on the target
136 | return defaultValueType.getAnimatableNone ? defaultValueType.getAnimatableNone(value) : undefined
137 | }
138 |
--------------------------------------------------------------------------------
/src/presets/roll.ts:
--------------------------------------------------------------------------------
1 | import type { MotionVariants } from '../types'
2 |
3 | // Roll from left
4 |
5 | export const rollLeft: MotionVariants = {
6 | initial: {
7 | x: -100,
8 | rotate: 90,
9 | opacity: 0,
10 | },
11 | enter: {
12 | x: 0,
13 | rotate: 0,
14 | opacity: 1,
15 | },
16 | }
17 |
18 | export const rollVisibleLeft: MotionVariants = {
19 | initial: {
20 | x: -100,
21 | rotate: 90,
22 | opacity: 0,
23 | },
24 | visible: {
25 | x: 0,
26 | rotate: 0,
27 | opacity: 1,
28 | },
29 | }
30 |
31 | export const rollVisibleOnceLeft: MotionVariants = {
32 | initial: {
33 | x: -100,
34 | rotate: 90,
35 | opacity: 0,
36 | },
37 | visibleOnce: {
38 | x: 0,
39 | rotate: 0,
40 | opacity: 1,
41 | },
42 | }
43 |
44 | // Roll from right
45 |
46 | export const rollRight: MotionVariants = {
47 | initial: {
48 | x: 100,
49 | rotate: -90,
50 | opacity: 0,
51 | },
52 | enter: {
53 | x: 0,
54 | rotate: 0,
55 | opacity: 1,
56 | },
57 | }
58 |
59 | export const rollVisibleRight: MotionVariants = {
60 | initial: {
61 | x: 100,
62 | rotate: -90,
63 | opacity: 0,
64 | },
65 | visible: {
66 | x: 0,
67 | rotate: 0,
68 | opacity: 1,
69 | },
70 | }
71 |
72 | export const rollVisibleOnceRight: MotionVariants = {
73 | initial: {
74 | x: 100,
75 | rotate: -90,
76 | opacity: 0,
77 | },
78 | visibleOnce: {
79 | x: 0,
80 | rotate: 0,
81 | opacity: 1,
82 | },
83 | }
84 |
85 | // Roll from top
86 |
87 | export const rollTop: MotionVariants = {
88 | initial: {
89 | y: -100,
90 | rotate: -90,
91 | opacity: 0,
92 | },
93 | enter: {
94 | y: 0,
95 | rotate: 0,
96 | opacity: 1,
97 | },
98 | }
99 |
100 | export const rollVisibleTop: MotionVariants = {
101 | initial: {
102 | y: -100,
103 | rotate: -90,
104 | opacity: 0,
105 | },
106 | visible: {
107 | y: 0,
108 | rotate: 0,
109 | opacity: 1,
110 | },
111 | }
112 |
113 | export const rollVisibleOnceTop: MotionVariants = {
114 | initial: {
115 | y: -100,
116 | rotate: -90,
117 | opacity: 0,
118 | },
119 | visibleOnce: {
120 | y: 0,
121 | rotate: 0,
122 | opacity: 1,
123 | },
124 | }
125 |
126 | // Roll from bottom
127 |
128 | export const rollBottom: MotionVariants = {
129 | initial: {
130 | y: 100,
131 | rotate: 90,
132 | opacity: 0,
133 | },
134 | enter: {
135 | y: 0,
136 | rotate: 0,
137 | opacity: 1,
138 | },
139 | }
140 |
141 | export const rollVisibleBottom: MotionVariants = {
142 | initial: {
143 | y: 100,
144 | rotate: 90,
145 | opacity: 0,
146 | },
147 | visible: {
148 | y: 0,
149 | rotate: 0,
150 | opacity: 1,
151 | },
152 | }
153 |
154 | export const rollVisibleOnceBottom: MotionVariants = {
155 | initial: {
156 | y: 100,
157 | rotate: 90,
158 | opacity: 0,
159 | },
160 | visibleOnce: {
161 | y: 0,
162 | rotate: 0,
163 | opacity: 1,
164 | },
165 | }
166 |
167 | export default {
168 | rollLeft,
169 | rollVisibleLeft,
170 | rollVisibleOnceLeft,
171 | rollRight,
172 | rollVisibleRight,
173 | rollVisibleOnceRight,
174 | rollTop,
175 | rollVisibleTop,
176 | rollVisibleOnceTop,
177 | rollBottom,
178 | rollVisibleBottom,
179 | rollVisibleOnceBottom,
180 | }
181 |
--------------------------------------------------------------------------------
/src/directive/index.ts:
--------------------------------------------------------------------------------
1 | import type { Directive, DirectiveBinding, Ref, VNode } from 'vue'
2 | import defu from 'defu'
3 | import { ref, toRaw, unref } from 'vue'
4 | import { motionState } from '../features/state'
5 | import type { MotionInstance, MotionVariants } from '../types'
6 | import { useMotion } from '../useMotion'
7 | import { resolveVariants } from '../utils/directive'
8 | import { variantToStyle } from '../utils/transform'
9 | import { registerVisibilityHooks } from '../features/visibilityHooks'
10 |
11 | export function directive(
12 | variants?: MotionVariants,
13 | isPreset = false,
14 | ): Directive {
15 | const register = (el: HTMLElement | SVGElement, binding: DirectiveBinding, node: VNode>) => {
16 | // Get instance key if possible (binding value or element key in case of v-for's)
17 | const key = (binding.value && typeof binding.value === 'string' ? binding.value : node.key) as string
18 |
19 | // Cleanup previous motion instance if it exists
20 | if (key && motionState[key])
21 | motionState[key].stop()
22 |
23 | // We deep copy presets to prevent global mutation
24 | const variantsObject = isPreset ? structuredClone(toRaw(variants) || {}) : variants || {}
25 |
26 | // Initialize variants with argument
27 | const variantsRef = ref(variantsObject) as Ref>
28 |
29 | // Set variants from v-motion binding
30 | if (typeof binding.value === 'object')
31 | variantsRef.value = binding.value
32 |
33 | // Resolve variants from node props
34 | resolveVariants(node, variantsRef)
35 |
36 | // Disable visibilityHooks, these will be registered in `mounted`
37 | const motionOptions = { eventListeners: true, lifeCycleHooks: true, syncVariants: true, visibilityHooks: false }
38 |
39 | // Create motion instance
40 | const motionInstance = useMotion(
41 | el,
42 | variantsRef as MotionVariants,
43 | motionOptions,
44 | )
45 |
46 | // Pass the motion instance via the local element
47 | // @ts-expect-error - we know that the element is a HTMLElement
48 | el.motionInstance = motionInstance
49 |
50 | // Set the global state reference if the name is set through v-motion="`value`"
51 | if (key)
52 | motionState[key] = motionInstance
53 | }
54 |
55 | const mounted = (
56 | el: (HTMLElement | SVGElement) & { motionInstance?: MotionInstance> },
57 | _binding: DirectiveBinding,
58 | _node: VNode> }, Record>,
59 | ) => {
60 | // Visibility hooks
61 | // eslint-disable-next-line ts/no-unused-expressions
62 | el.motionInstance && registerVisibilityHooks(el.motionInstance)
63 | }
64 |
65 | return {
66 | created: register,
67 | mounted,
68 | getSSRProps(binding, node) {
69 | // Get initial value from binding
70 | let { initial: bindingInitial } = binding.value || (node && node?.props) || {}
71 |
72 | bindingInitial = unref(bindingInitial)
73 |
74 | // Merge it with directive initial variants
75 | const initial = defu({}, variants?.initial || {}, bindingInitial || {})
76 |
77 | // No initial
78 | if (!initial || Object.keys(initial).length === 0)
79 | return
80 |
81 | // Resolve variant
82 | const style = variantToStyle(initial)
83 |
84 | return {
85 | style,
86 | }
87 | },
88 | }
89 | }
90 |
91 | export default directive
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🤹 @vueuse/motion
2 |
3 | [](https://www.npmjs.com/package/@vueuse/motion)
4 | [](https://www.npmjs.com/package/vueuse-motion-nightly)
5 | [](https://npm-stat.com/charts.html?package=@vueuse/motion)
6 | [](https://www.npmjs.com/package/@vueuse/motion)
7 | [](https://app.netlify.com/sites/vueuse-motion/deploys)
8 |
9 | Vue Composables putting your components in motion
10 |
11 | - 🏎 **Smooth animations** based on [Popmotion](https://popmotion.io/)
12 | - 🎮 **Declarative** API inspired by [Framer Motion](https://www.framer.com/motion/)
13 | - 🚀 **Plug** & **play** with **20+ presets**
14 | - 🌐 **SSR Ready**
15 | - 🚚 First-class support for **Nuxt 3**
16 | - ✨ Written in **TypeScript**
17 | - 🏋️♀️ Lightweight with **<25kb** bundle size
18 |
19 | [🌍 Documentation](https://motion.vueuse.org)
20 |
21 | [👀 Demos](https://vueuse-motion-demo.netlify.app)
22 |
23 | ## Quick Start
24 |
25 | Let's get started by installing the package and adding the plugin.
26 |
27 | From your terminal:
28 |
29 | ```bash
30 | npm install @vueuse/motion
31 | ```
32 |
33 | In your Vue app entry file:
34 |
35 | ```javascript
36 | import { createApp } from "vue";
37 | import { MotionPlugin } from "@vueuse/motion";
38 | import App from "./App.vue";
39 |
40 | const app = createApp(App);
41 |
42 | app.use(MotionPlugin);
43 |
44 | app.mount("#app");
45 | ```
46 |
47 | You can now animate any of your component, HTML or SVG elements using `v-motion`.
48 |
49 | ```vue
50 |
51 |
62 |
63 | ```
64 |
65 | To see more about how to use directives, check out [Directive Usage](https://motion.vueuse.org/features/directive-usage).
66 |
67 | To see more about what properties you can animate, check out [Motion Properties](https://motion.vueuse.org/features/motion-properties).
68 |
69 | To see more about how to create your own animation styles, check out [Transition Properties](https://motion.vueuse.org/features/transition-properties).
70 |
71 | To see more about what are variants and how you can use them, check out [Variants](https://motion.vueuse.org/features/variants).
72 |
73 | To see more about how to control your declared variants, check out [Motion Instance](https://motion.vueuse.org/features/motion-instance).
74 |
75 | ## Nightly release channel
76 |
77 | You can try out the latest changes before a stable release by installing the nightly release channel.
78 |
79 | ```bash
80 | npm install @vueuse/motion@npm:vueuse-motion-nightly
81 | ```
82 |
83 | ## Credits
84 |
85 | This package is heavily inspired by [Framer Motion](https://www.framer.com/motion/) by [@mattgperry](https://twitter.com/mattgperry).
86 |
87 | If you are interested in using [WAAPI](https://developer.mozilla.org/fr/docs/Web/API/Web_Animations_API), check out [Motion.dev](https://motion.dev/)!
88 |
89 | I would also like to thank [antfu](https://github.com/antfu), [patak-dev](https://github.com/patak-dev) and [kazupon](https://github.com/kazupon) for their kind help!
90 |
91 | If you like this package, consider following me on [GitHub](https://github.com/Tahul) and on [Twitter](https://twitter.com/yaeeelglx).
92 |
93 | 👋
94 |
--------------------------------------------------------------------------------
/docs/components/content/PresetSection.vue:
--------------------------------------------------------------------------------
1 |
54 |
55 |
56 |
57 |
58 | {{ name.replace(/[A-Z]/g, (s: any) => ` ${s}`) }}
59 |
60 |
61 |
62 |
63 |
64 | {{ `
65 |
66 | ` }}
67 |
68 |
69 |
70 |
89 |
90 |
91 |
92 |
93 |
153 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v3.0.3 (2025-03-10T23:25:32Z)
2 |
3 | This changelog is generated by [GitHub Releases](https://github.com/vueuse/motion/releases/tag/v3.0.3)
4 |
5 | ### 🐞 Bug Fixes
6 |
7 | - Nuxt build externalize types - by @BobbieGoede [(532cf)](https://github.com/vueuse/motion/commit/532cfc8)
8 | - Add `defu` as dependency - by @BobbieGoede [(ce62d)](https://github.com/vueuse/motion/commit/ce62df4)
9 |
10 | ##### [View changes on GitHub](https://github.com/vueuse/motion/compare/v3.0.2...v3.0.3)
11 |
12 |
13 | # v3.0.2 (2025-03-10T20:37:16Z)
14 |
15 | This changelog is generated by [GitHub Releases](https://github.com/vueuse/motion/releases/tag/v3.0.2)
16 |
17 | ### 🐞 Bug Fixes
18 |
19 | - Remove `csstype` and update `vue` - by @BobbieGoede [(143c2)](https://github.com/vueuse/motion/commit/143c21a)
20 | - Remove `csstype` from tsconfigs - by @BobbieGoede [(8ad06)](https://github.com/vueuse/motion/commit/8ad06ef)
21 | - Externalize css types by adding type annotations - by @BobbieGoede [(43221)](https://github.com/vueuse/motion/commit/43221cb)
22 | - Move `vue` back to `dependencies` - by @BobbieGoede [(40b4d)](https://github.com/vueuse/motion/commit/40b4d97)
23 | - Move `vue` back to `devDependencies` - by @BobbieGoede [(9b6aa)](https://github.com/vueuse/motion/commit/9b6aab4)
24 | - Use `Component` type from `vue` - by @BobbieGoede [(c2995)](https://github.com/vueuse/motion/commit/c2995ff)
25 | - Use `import.meta.env.DEV` to detect development environment - by @BobbieGoede [(81702)](https://github.com/vueuse/motion/commit/8170220)
26 | - Use `import.meta.env` to check environment - by @BobbieGoede [(ad270)](https://github.com/vueuse/motion/commit/ad27084)
27 | - Use `import.meta.env.MODE` to check environment - by @BobbieGoede [(c83fc)](https://github.com/vueuse/motion/commit/c83fc77)
28 | - Access `meta.env.X` with optional chaining - by @BobbieGoede [(fb9ed)](https://github.com/vueuse/motion/commit/fb9ede7)
29 |
30 | ##### [View changes on GitHub](https://github.com/vueuse/motion/compare/v3.0.1...v3.0.2)
31 |
32 |
33 | # v3.0.1 (2025-03-10T14:08:52Z)
34 |
35 | This changelog is generated by [GitHub Releases](https://github.com/vueuse/motion/releases/tag/v3.0.1)
36 |
37 | *No significant changes*
38 |
39 | ##### [View changes on GitHub](https://github.com/vueuse/motion/compare/v3.0.0...v3.0.1)
40 |
41 |
42 | # v2.2.6 (2024-10-11T18:52:24Z)
43 |
44 | This changelog is generated by [GitHub Releases](https://github.com/vueuse/motion/releases/tag/v2.2.6)
45 |
46 | ### 🐞 Bug Fixes
47 |
48 | - Export `useMotionFeatures` - by @NoelDeMartin in https://github.com/vueuse/motion/issues/235 [(c5b16)](https://github.com/vueuse/motion/commit/c5b16ca)
49 | - Dev environment variable undefined - by @BobbieGoede in https://github.com/vueuse/motion/issues/236 [(bd6fa)](https://github.com/vueuse/motion/commit/bd6fa4d)
50 |
51 | ##### [View changes on GitHub](https://github.com/vueuse/motion/compare/v2.2.5...v2.2.6)
52 |
53 |
54 | # v2.2.5 (2024-09-06T13:35:32Z)
55 |
56 | This changelog is generated by [GitHub Releases](https://github.com/vueuse/motion/releases/tag/v2.2.5)
57 |
58 | ### 🐞 Bug Fixes
59 |
60 | - **types**: Improve `MotionPlugin` types - by @cjboy76 in https://github.com/vueuse/motion/issues/231 [(f6983)](https://github.com/vueuse/motion/commit/f6983db)
61 |
62 | ##### [View changes on GitHub](https://github.com/vueuse/motion/compare/v2.2.4...v2.2.5)
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/useMotionControls.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeRef } from 'vue'
2 | import { isObject } from '@vueuse/core'
3 | import { ref, unref, watch } from 'vue'
4 | import type { MotionControls, MotionProperties, MotionTransitions, MotionVariants, Variant } from './types'
5 | import { useMotionTransitions } from './useMotionTransitions'
6 | import { getDefaultTransition } from './utils/defaults'
7 |
8 | /**
9 | * A Composable handling motion controls, pushing resolved variant to useMotionTransitions manager.
10 | */
11 | export function useMotionControls>(
12 | motionProperties: MotionProperties,
13 | variants: MaybeRef = {} as MaybeRef,
14 | { motionValues, push, stop }: MotionTransitions = useMotionTransitions(),
15 | ): MotionControls {
16 | // Variants as ref
17 | const _variants = unref(variants) as V
18 |
19 | // Is the current instance animated ref
20 | const isAnimating = ref(false)
21 |
22 | // Watcher setting isAnimating
23 | watch(
24 | motionValues,
25 | (newVal) => {
26 | // Go through every motion value, and check if any is animating
27 | isAnimating.value = Object.values(newVal).filter(value => value.isAnimating()).length > 0
28 | },
29 | {
30 | immediate: true,
31 | deep: true,
32 | },
33 | )
34 |
35 | const getVariantFromKey = (variant: keyof V): Variant => {
36 | if (!_variants || !_variants[variant])
37 | throw new Error(`The variant ${variant as string} does not exist.`)
38 |
39 | return _variants[variant] as Variant
40 | }
41 |
42 | const apply = (variant: Variant | keyof V): Promise | undefined => {
43 | // If variant is a key, try to resolve it
44 | if (typeof variant === 'string')
45 | variant = getVariantFromKey(variant)
46 |
47 | // Create promise chain for each animated property
48 | const animations = Object.entries(variant)
49 | .map(([key, value]) => {
50 | // Skip transition key
51 | if (key === 'transition')
52 | return undefined
53 |
54 | return new Promise(resolve =>
55 | // @ts-expect-error - Fix errors later for typescript 5
56 | push(key as keyof MotionProperties, value, motionProperties, (variant as Variant).transition || getDefaultTransition(key, variant[key]), resolve),
57 | )
58 | })
59 | .filter(Boolean)
60 |
61 | // Call `onComplete` after all animations have completed
62 | async function waitForComplete() {
63 | await Promise.all(animations)
64 | ;(variant as Variant).transition?.onComplete?.()
65 | }
66 |
67 | // Return using `Promise.all` to preserve type compatibility
68 | return Promise.all([waitForComplete()])
69 | }
70 |
71 | const set = (variant: Variant | keyof V) => {
72 | // Get variant data from parameter
73 | const variantData = isObject(variant) ? variant : getVariantFromKey(variant)
74 |
75 | // Set in chain
76 | Object.entries(variantData).forEach(([key, value]) => {
77 | // Skip transition key
78 | if (key === 'transition')
79 | return
80 |
81 | push(key as keyof MotionProperties, value, motionProperties, {
82 | immediate: true,
83 | })
84 | })
85 | }
86 |
87 | const leave = async (done: () => void) => {
88 | let leaveVariant: Variant | undefined
89 |
90 | if (_variants) {
91 | if (_variants.leave)
92 | leaveVariant = _variants.leave
93 |
94 | if (!_variants.leave && _variants.initial)
95 | leaveVariant = _variants.initial
96 | }
97 |
98 | if (!leaveVariant) {
99 | done()
100 | return
101 | }
102 |
103 | await apply(leaveVariant)
104 |
105 | done()
106 | }
107 |
108 | return {
109 | isAnimating,
110 | apply,
111 | set,
112 | leave,
113 | stop,
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@vueuse/motion",
3 | "type": "module",
4 | "version": "3.0.3",
5 | "packageManager": "pnpm@10.6.2",
6 | "description": "🤹 Vue Composables putting your components in motion",
7 | "author": "Yaël GUILLOUX ",
8 | "contributors": [
9 | {
10 | "name": "Yaël Guilloux (@Tahul)"
11 | },
12 | {
13 | "name": "Bobbie Goede (@BobbieGoede)"
14 | }
15 | ],
16 | "license": "MIT",
17 | "homepage": "https://github.com/vueuse/motion#readme",
18 | "repository": "https://github.com/vueuse/motion",
19 | "bugs": {
20 | "url": "https://github.com/vueuse/motion/issues"
21 | },
22 | "keywords": [
23 | "vue",
24 | "hook",
25 | "motion",
26 | "animation",
27 | "v-motion",
28 | "popmotion-vue"
29 | ],
30 | "sideEffects": false,
31 | "exports": {
32 | ".": "./dist/index.mjs",
33 | "./nuxt": "./dist/nuxt/module.mjs"
34 | },
35 | "main": "./dist/index.mjs",
36 | "module": "./dist/index.mjs",
37 | "types": "./dist/index.d.mts",
38 | "typesVersions": {
39 | "*": {
40 | "nuxt": [
41 | "./dist/nuxt/module.d.mts"
42 | ]
43 | }
44 | },
45 | "files": [
46 | "LICENSE",
47 | "README.md",
48 | "dist"
49 | ],
50 | "scripts": {
51 | "build": "unbuild && pnpm build:nuxt-module",
52 | "build:nuxt-module": "nuxt-module-build build ./src/nuxt --outDir ../../dist/nuxt",
53 | "dev": "pnpm dev:vite",
54 | "lint": "eslint .",
55 | "lint:fix": "eslint . --fix",
56 | "test:unit": "vitest run",
57 | "test:coverage": "vitest run --coverage",
58 | "test": "pnpm test:unit && pnpm test:coverage",
59 | "prepare": "pnpm prepare:nuxt && pnpm prepare:docs",
60 | "prepack": "pnpm build",
61 | "release": "bumpp --commit \"release: v%s\" --push --tag",
62 | "changelog": "gh-changelogen --repo=vueuse/motion",
63 | "__": "__",
64 | "dev:nuxt": "(cd playgrounds/nuxt && pnpm dev:nuxt)",
65 | "build:nuxt": "(cd playgrounds/nuxt && pnpm build:nuxt)",
66 | "generate:nuxt": "(cd playgrounds/nuxt && pnpm preview:nuxt)",
67 | "dev:ssg": "(cd playgrounds/vite-ssg && pnpm dev:ssg)",
68 | "build:ssg": "(cd playgrounds/vite-ssg && pnpm build:ssg)",
69 | "preview:ssg": "(cd playgrounds/vite-ssg && pnpm preview:ssg)",
70 | "dev:vite": "(cd playgrounds/vite && pnpm dev:vite)",
71 | "build:vite": "(cd playgrounds/vite && pnpm build:vite)",
72 | "preview:vite": "(cd playgrounds/vite && pnpm preview:vite)",
73 | "dev:docs": "(cd docs && pnpm dev:docs)",
74 | "build:docs": "(cd docs && pnpm build:docs)",
75 | "preview:docs": "(cd docs && pnpm preview:docs)",
76 | "prepare:nuxt": "(cd playgrounds/nuxt && pnpm prepare:nuxt)",
77 | "prepare:docs": "(cd docs && pnpm prepare:docs)"
78 | },
79 | "peerDependencies": {
80 | "vue": ">=3.0.0"
81 | },
82 | "dependencies": {
83 | "@vueuse/core": "^13.0.0",
84 | "@vueuse/shared": "^13.0.0",
85 | "defu": "^6.1.4",
86 | "framesync": "^6.1.2",
87 | "popmotion": "^11.0.5",
88 | "style-value-types": "^5.1.2"
89 | },
90 | "optionalDependencies": {
91 | "@nuxt/kit": "^3.13.0"
92 | },
93 | "devDependencies": {
94 | "@antfu/eslint-config": "^2.19.1",
95 | "@nuxt/kit": "^3.13.0",
96 | "@nuxt/module-builder": "^0.8.3",
97 | "@nuxt/schema": "^3.13.0",
98 | "@vitest/coverage-v8": "^1.6.0",
99 | "@vue/test-utils": "^2.4.6",
100 | "bumpp": "^9.5.2",
101 | "changelogithub": "^0.13.10",
102 | "chokidar": "^3.6.0",
103 | "eslint": "^9.3.0",
104 | "gh-changelogen": "^0.2.8",
105 | "happy-dom": "^14.12.0",
106 | "jiti": "^1.21.6",
107 | "lint-staged": "^15.2.5",
108 | "nuxt": "^3.13.0",
109 | "pkg-pr-new": "^0.0.20",
110 | "typescript": "^5.8.2",
111 | "unbuild": "^3.5.0",
112 | "vite": "5.2.12",
113 | "vitest": "^1.6.0",
114 | "vue": "^3.5.13",
115 | "yorkie": "^2.0.0"
116 | },
117 | "pnpm": {
118 | "peerDependencyRules": {
119 | "ignoreMissing": [
120 | "@algolia/client-search",
121 | "@types/react",
122 | "react",
123 | "react-dom",
124 | "webpack",
125 | "postcss",
126 | "tailwindcss",
127 | "vue",
128 | "axios"
129 | ],
130 | "allowedVersions": {
131 | "axios": "^0.25.0",
132 | "vite": "^4.0.0"
133 | }
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/docs/content/2.features/1.directive-usage.md:
--------------------------------------------------------------------------------
1 | # Directive Usage
2 |
3 | vueuse/motion allows you to write your animations right from the template of your components without having to wrap the target elements in any wrapper component.
4 |
5 | The directive is expected to work the same whether you use it on a HTML or SVG element, or on any Vue component.
6 |
7 | ## Your first v-motion
8 |
9 | v-motion is the name of the directive from this package.
10 |
11 | The directive usage allows you to write your variants right from the template of your components.
12 |
13 | The v-motion can be used as many times you want in any and on any HTML or SVG component.
14 |
15 | Once put on an element, the v-motion will allow you to write your variants as props of this element.
16 |
17 | The supported variants props are the following:
18 |
19 | - **`initial`**: The properties the element will equip before it is mounted.
20 | - **`enter`**: The properties the element will equip after it is mounted.
21 | - **`visible`**: The properties the element will equip every time it is within view. Once it is out of view, the `initial` properties will be applied.
22 | - **`visible-once`**: The properties the element will equip once it is within view.
23 | - **`hovered`**: The properties the element will equip when the pointer enters its area.
24 | - **`focused`**: The properties the element will equip when the element receives focus.
25 | - **`tapped`**: The properties the element will equip upon clicking (mouse) or tapping (touch devices).
26 |
27 | You can also pass your variants as an object using the `:variants` prop.
28 |
29 | The `:variants` prop will be combined with all the other native variants properties, allowing you to define only your custom variants from this object.
30 |
31 | The rest of the variants properties can be found on the [Variants](/features/variants) page.
32 |
33 | ### Shorthand props
34 |
35 | For convenience we support the following shorthand props which allow you to quickly configure transition properties:
36 |
37 | - **`delay`**
38 | - **`duration`**
39 |
40 | If you specified a `visible`, `visible-once` or `enter` variant, these shorthand properties will be applied to each of them.
41 |
42 | Otherwise, they will be applied on the `initial` [variant](/features/variants) instead.
43 |
44 | ```vue
45 |
46 |
55 |
56 | ```
57 |
58 | ##### _Directives are amazing_ 😍
59 |
60 | ## Access a v-motion instance
61 |
62 | When defined from template, the target element might not be assigned to a ref.
63 |
64 | You can access motions controls using [useMotions](/api/use-motions).
65 |
66 | If you want to access a v-motion, you will have to give the element a name as v-motion value.
67 |
68 | Then you can just call useMotions, and get access to that v-motion controls using its name as a key.
69 |
70 | ```vue
71 |
72 |
78 |
79 |
80 |
91 | ```
92 |
93 | In the above example, the custom object will be an instance of [Motion Instance](/features/motion-instance).
94 |
95 | ### Custom Directives
96 |
97 | You can add custom directives that will be prefixed by `v-motion` right from the plugin config.
98 |
99 | ```javascript
100 | import { MotionPlugin } from '@vueuse/motion'
101 |
102 | const app = createApp(App)
103 |
104 | app.use(MotionPlugin, {
105 | directives: {
106 | 'pop-bottom': {
107 | initial: {
108 | scale: 0,
109 | opacity: 0,
110 | y: 100,
111 | },
112 | visible: {
113 | scale: 1,
114 | opacity: 1,
115 | y: 0,
116 | },
117 | },
118 | },
119 | })
120 |
121 | app.mount('#app')
122 | ```
123 |
124 | With the code above, you will have access to `v-motion-pop-bottom` globally on any element or component of the app.
125 |
--------------------------------------------------------------------------------
/docs/content/2.features/4.transition-properties.md:
--------------------------------------------------------------------------------
1 | # Transition Properties
2 |
3 | Transition properties are represented by an object containing all transition parameters of a variant.
4 |
5 | They are one of the two parts that compose a [Variant](/features/variants), with [Motion Properties](/features/motion-properties).
6 |
7 | ## Orchestration
8 |
9 | ### Delay
10 |
11 | You can specify a delay which will be added every time the transition is pushed.
12 |
13 | ```vue
14 |
26 | ```
27 |
28 | ##### _This animation will be throttled of 1 second._ ☝️
29 |
30 | ### Repeat
31 |
32 | The native [Popmotion Repeat](https://popmotion.io/#quick-start-animation-animate-options-repeat) feature is supported.
33 |
34 | Three parameters are available:
35 |
36 | - `repeat` that is the number of times the animation will be repeated. Can be set to `Infinity`.
37 |
38 | - `repeatDelay`, a duration in milliseconds to wait before repeating the animation.
39 |
40 | - `repeatType` that supports `loop`, `mirror`, `reverse`. The default is `loop`.
41 |
42 | ```vue
43 |
56 | ```
57 |
58 | ##### _Zboing!._ ☝️
59 |
60 | ## Transition Types
61 |
62 | Two types of animations are supported.
63 |
64 | For the most [Common Animatable Properties](https://github.com/vueuse/motion/blob/main/src/utils/defaults.ts#L43), it will uses generated spring transitions.
65 |
66 | The rest of the properties might be using keyframes.
67 |
68 | ### Spring
69 |
70 | Springs are used to create dynamic and natural animations.
71 |
72 | It supports multiple parameters:
73 |
74 | - `stiffness`
75 |
76 | A higher stiffness will result in a snappier animation.
77 |
78 | - `damping`
79 |
80 | The opposite of stiffness. The lower it is relative to sitffness, the bouncier the animation will get.
81 |
82 | - `mass`
83 |
84 | The mass of the object, heavier objects will take longer to speed up and slow down.
85 |
86 | ```vue
87 |
104 | ```
105 |
106 | ### Keyframes
107 |
108 | Keyframes ared used mainly for color related animations as springs are not designed for that.
109 |
110 | It also works with numbers though.
111 |
112 | It supports multiple parameters:
113 |
114 | - `duration`
115 |
116 | The duration of the animation, in milliseconds.
117 |
118 | Defaults to `800`.
119 |
120 | - `ease`
121 |
122 | Supports multiples types:
123 |
124 | - An easing name
125 | - Array of easing names
126 | - Easing function
127 | - Array of easing
128 | - A cubic bezier definition using a 4 numbers array
129 |
130 | Supported easing names:
131 |
132 | - **linear**
133 | - **easeIn**, **easeOut**, **easeInOut**
134 | - **circIn**, **circOut**, **circInOut**
135 | - **backIn**, **backOut**, **backInOut**
136 | - **anticipate**
137 |
138 |
139 | ```vue
140 |
156 | ```
157 |
158 | ## Per-key transition definition
159 |
160 | Transition properties supports per-key transition definition.
161 |
162 | It allows you to create complex animations without using the `apply` function.
163 |
164 | To do so, you have to define key-specific transition inside your transition definition.
165 |
166 | ```vue
167 |
186 | ```
187 |
188 | ##### _The `y` transition will start when the `opacity` one is over._ ☝️
189 |
--------------------------------------------------------------------------------
/docs/components/content/Hero.vue:
--------------------------------------------------------------------------------
1 |
64 |
65 |
66 |
91 |
92 |
93 |
214 |
--------------------------------------------------------------------------------
/src/motionValue.ts:
--------------------------------------------------------------------------------
1 | import type { FrameData } from 'framesync'
2 | import sync, { getFrameData } from 'framesync'
3 | import { velocityPerSecond } from 'popmotion'
4 | import type { StartAnimation, Subscriber } from './types'
5 | import { SubscriptionManager } from './utils/subscription-manager'
6 |
7 | function isFloat(value: any): value is string {
8 | return !Number.isNaN(Number.parseFloat(value))
9 | }
10 |
11 | /**
12 | * `MotionValue` is used to track the state and velocity of motion values.
13 | */
14 | export class MotionValue {
15 | /**
16 | * The current state of the `MotionValue`.
17 | */
18 | private current: V
19 |
20 | /**
21 | * The previous state of the `MotionValue`.
22 | */
23 | private prev: V
24 |
25 | /**
26 | * Duration, in milliseconds, since last updating frame.
27 | */
28 | private timeDelta = 0
29 |
30 | /**
31 | * Timestamp of the last time this `MotionValue` was updated.
32 | */
33 | private lastUpdated = 0
34 |
35 | /**
36 | * Functions to notify when the `MotionValue` updates.
37 | */
38 | updateSubscribers = new SubscriptionManager>()
39 |
40 | /**
41 | * A reference to the currently-controlling Popmotion animation
42 | */
43 | private stopAnimation?: null | (() => void)
44 |
45 | /**
46 | * Tracks whether this value can output a velocity.
47 | */
48 | private canTrackVelocity = false
49 |
50 | /**
51 | * init - The initiating value
52 | * config - Optional configuration options
53 | */
54 | constructor(init: V) {
55 | this.prev = this.current = init
56 | this.canTrackVelocity = isFloat(this.current)
57 | }
58 |
59 | /**
60 | * Adds a function that will be notified when the `MotionValue` is updated.
61 | *
62 | * It returns a function that, when called, will cancel the subscription.
63 | */
64 | onChange(subscription: Subscriber): () => void {
65 | return this.updateSubscribers.add(subscription)
66 | }
67 |
68 | clearListeners() {
69 | this.updateSubscribers.clear()
70 | }
71 |
72 | /**
73 | * Sets the state of the `MotionValue`.
74 | *
75 | * @param v
76 | * @param render
77 | */
78 | set(v: V) {
79 | this.updateAndNotify(v)
80 | }
81 |
82 | /**
83 | * Update and notify `MotionValue` subscribers.
84 | *
85 | * @param v
86 | * @param render
87 | */
88 | updateAndNotify = (v: V) => {
89 | // Update values
90 | this.prev = this.current
91 | this.current = v
92 |
93 | // Get frame data
94 | const { delta, timestamp } = getFrameData()
95 |
96 | // Update timestamp
97 | if (this.lastUpdated !== timestamp) {
98 | this.timeDelta = delta
99 | this.lastUpdated = timestamp
100 | }
101 |
102 | // Schedule velocity check post frame render
103 | sync.postRender(this.scheduleVelocityCheck)
104 |
105 | // Update subscribers
106 | this.updateSubscribers.notify(this.current)
107 | }
108 |
109 | /**
110 | * Returns the latest state of `MotionValue`
111 | *
112 | * @returns - The latest state of `MotionValue`
113 | */
114 | get() {
115 | return this.current
116 | }
117 |
118 | /**
119 | * Get previous value.
120 | *
121 | * @returns - The previous latest state of `MotionValue`
122 | */
123 | getPrevious() {
124 | return this.prev
125 | }
126 |
127 | /**
128 | * Returns the latest velocity of `MotionValue`
129 | *
130 | * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
131 | */
132 | getVelocity() {
133 | // This could be isFloat(this.prev) && isFloat(this.current), but that would be wasteful
134 | // These casts could be avoided if parseFloat would be typed better
135 | return this.canTrackVelocity ? velocityPerSecond(Number.parseFloat(this.current as any) - Number.parseFloat(this.prev as any), this.timeDelta) : 0
136 | }
137 |
138 | /**
139 | * Schedule a velocity check for the next frame.
140 | */
141 | private scheduleVelocityCheck = () => sync.postRender(this.velocityCheck)
142 |
143 | /**
144 | * Updates `prev` with `current` if the value hasn't been updated this frame.
145 | * This ensures velocity calculations return `0`.
146 | */
147 | private velocityCheck = ({ timestamp }: FrameData) => {
148 | if (!this.canTrackVelocity)
149 | this.canTrackVelocity = isFloat(this.current)
150 |
151 | if (timestamp !== this.lastUpdated)
152 | this.prev = this.current
153 | }
154 |
155 | /**
156 | * Registers a new animation to control this `MotionValue`. Only one
157 | * animation can drive a `MotionValue` at one time.
158 | */
159 | start(animation: StartAnimation) {
160 | this.stop()
161 |
162 | return new Promise((resolve) => {
163 | const { stop } = animation(resolve as () => void)
164 |
165 | this.stopAnimation = stop
166 | }).then(() => this.clearAnimation())
167 | }
168 |
169 | /**
170 | * Stop the currently active animation.
171 | */
172 | stop() {
173 | if (this.stopAnimation)
174 | this.stopAnimation()
175 |
176 | this.clearAnimation()
177 | }
178 |
179 | /**
180 | * Returns `true` if this value is currently animating.
181 | */
182 | isAnimating() {
183 | return !!this.stopAnimation
184 | }
185 |
186 | /**
187 | * Clear the current animation reference.
188 | */
189 | private clearAnimation() {
190 | this.stopAnimation = null
191 | }
192 |
193 | /**
194 | * Destroy and clean up subscribers to this `MotionValue`.
195 | */
196 | destroy() {
197 | this.updateSubscribers.clear()
198 | this.stop()
199 | }
200 | }
201 |
202 | export function getMotionValue(init: V) {
203 | return new MotionValue(init)
204 | }
205 |
--------------------------------------------------------------------------------