├── .github ├── FUNDING.yml ├── workflows │ ├── reproduire.yml │ ├── reproduire-close.yml │ ├── targets.yml │ ├── nightly-release.yml │ ├── release.yaml │ └── ci.yml ├── reproduire │ └── needs-reproduction.md ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ └── bug-report.yml ├── docs ├── content │ ├── 3.api │ │ ├── _dir.yml │ │ ├── 8.use-motion-features.md │ │ ├── 5.use-motion-variants.md │ │ ├── 3.use-motions.md │ │ ├── 12.reactive-transform.md │ │ ├── 9.use-element-style.md │ │ ├── 10.use-element-transform.md │ │ ├── 6.use-motion-transitions.md │ │ ├── 11.reactive-style.md │ │ ├── 4.use-motion-properties.md │ │ ├── 7.use-motion-controls.md │ │ ├── 1.use-motion.md │ │ └── 2.use-spring.md │ ├── 2.features │ │ ├── 0.presets.md │ │ ├── 2.composable-usage.md │ │ ├── 3.motion-properties.md │ │ ├── 7.components.md │ │ ├── 6.motion-instance.md │ │ ├── 1.directive-usage.md │ │ └── 4.transition-properties.md │ ├── index.md │ └── 1.getting-started │ │ ├── 1.introduction.md │ │ └── 2.nuxt.md ├── tsconfig.json ├── public │ ├── banner.png │ └── favicon.ico ├── tokens.config.ts ├── package.json ├── nuxt.config.ts ├── app.config.ts └── components │ └── content │ ├── examples │ ├── MotionComponent.vue │ └── MotionGroupComponent.vue │ ├── PresetsViewer.vue │ ├── Features.vue │ ├── PresetSection.vue │ └── Hero.vue ├── playgrounds ├── vite │ ├── .npmrc │ ├── public │ │ ├── logo.png │ │ └── favicon.ico │ ├── src │ │ ├── components │ │ │ ├── Block.vue │ │ │ ├── CodeBlock.vue │ │ │ └── DemoBox.vue │ │ ├── examples │ │ │ ├── basic.ts │ │ │ ├── transitions.ts │ │ │ └── delay.ts │ │ ├── main.ts │ │ ├── demos │ │ │ ├── Sandbox.vue │ │ │ ├── Transitions.vue │ │ │ ├── Editor.vue │ │ │ └── Delay.vue │ │ ├── index.css │ │ └── App.vue │ ├── windi.config.ts │ ├── index.html │ ├── vite.config.ts │ └── package.json ├── nuxt │ ├── tsconfig.json │ ├── app.vue │ ├── pages │ │ ├── index.vue │ │ └── test.vue │ ├── package.json │ ├── components │ │ └── content │ │ │ └── Block.vue │ ├── nuxt.config.ts │ └── content │ │ └── index.md └── vite-ssg │ ├── src │ ├── main.ts │ └── App.vue │ ├── vue-shim.d.ts │ ├── index.html │ ├── package.json │ └── vite.config.ts ├── .npmrc ├── pnpm-workspace.yaml ├── src ├── useMotions.ts ├── utils │ ├── keys.ts │ ├── type-feature.ts │ ├── events.ts │ ├── is-motion-instance.ts │ ├── element.ts │ ├── subscription-manager.ts │ ├── slugify.ts │ ├── defaults.ts │ ├── directive.ts │ ├── transform.ts │ ├── transform-parser.ts │ └── style.ts ├── components │ ├── index.ts │ ├── Motion.ts │ └── MotionGroup.ts ├── nuxt │ ├── src │ │ ├── runtime │ │ │ ├── templates │ │ │ │ ├── motion.d.ts │ │ │ │ └── motion.ts │ │ │ └── composables │ │ │ │ └── index.ts │ │ └── module.ts │ ├── build.config.ts │ └── tsconfig.json ├── features │ ├── state.ts │ ├── syncVariants.ts │ ├── visibilityHooks.ts │ ├── lifeCycleHooks.ts │ └── eventListeners.ts ├── types │ ├── index.ts │ ├── nuxt.ts │ ├── plugin.ts │ ├── value.ts │ ├── variants.ts │ └── instance.ts ├── useReducedMotion.ts ├── usePermissiveTarget.ts ├── presets │ ├── fade.ts │ ├── pop.ts │ ├── index.ts │ ├── slide.ts │ └── roll.ts ├── useMotionVariants.ts ├── index.ts ├── useMotionTransitions.ts ├── useMotionFeatures.ts ├── reactiveStyle.ts ├── useMotion.ts ├── useSpring.ts ├── useMotionValues.ts ├── useElementTransform.ts ├── plugin │ └── index.ts ├── useMotionProperties.ts ├── reactiveTransform.ts ├── useElementStyle.ts ├── directive │ └── index.ts ├── useMotionControls.ts └── motionValue.ts ├── env.d.ts ├── .editorconfig ├── netlify.toml ├── vite.config.ts ├── eslint.config.js ├── scripts ├── release.sh ├── release-nightly.sh └── bump-nightly.ts ├── .gitignore ├── tests ├── reactiveStyle.spec.ts ├── isMotionInstance.spec.ts ├── useMotion.spec.ts ├── useMotionTransitions.spec.ts ├── useElementStyle.spec.ts ├── useElementTransform.spec.ts ├── utils │ ├── index.ts │ └── intersectionObserver.ts └── reactiveTransform.spec.ts ├── LICENSE ├── tsconfig.json ├── README.md ├── CHANGELOG.md └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Tahul] 2 | -------------------------------------------------------------------------------- /docs/content/3.api/_dir.yml: -------------------------------------------------------------------------------- 1 | title: API 2 | -------------------------------------------------------------------------------- /playgrounds/vite/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /playgrounds/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /docs/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueuse/motion/HEAD/docs/public/banner.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueuse/motion/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/tokens.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from 'pinceau' 2 | 3 | export default defineTheme({}) 4 | -------------------------------------------------------------------------------- /playgrounds/nuxt/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playgrounds/vite/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueuse/motion/HEAD/playgrounds/vite/public/logo.png -------------------------------------------------------------------------------- /playgrounds/vite/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueuse/motion/HEAD/playgrounds/vite/public/favicon.ico -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | - playgrounds/vite 4 | - playgrounds/nuxt 5 | - playgrounds/vite-ssg 6 | -------------------------------------------------------------------------------- /playgrounds/nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/useMotions.ts: -------------------------------------------------------------------------------- 1 | import { motionState } from './features/state' 2 | 3 | export function useMotions() { 4 | return motionState 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/keys.ts: -------------------------------------------------------------------------------- 1 | export const CUSTOM_PRESETS = Symbol( 2 | import.meta.env?.MODE === 'development' ? 'motionCustomPresets' : '', 3 | ) 4 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MotionComponent } from './Motion' 2 | export { default as MotionGroupComponent } from './MotionGroup' 3 | -------------------------------------------------------------------------------- /src/nuxt/src/runtime/templates/motion.d.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'nuxt/app' 2 | 3 | declare const plugin: Plugin> 4 | export default plugin 5 | -------------------------------------------------------------------------------- /playgrounds/vite/src/components/Block.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/features/state.ts: -------------------------------------------------------------------------------- 1 | import type { MotionInstanceBindings, MotionVariants } from '../types' 2 | 3 | export const motionState: MotionInstanceBindings> = {} 4 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './instance' 2 | export * from './plugin' 3 | export * from './transitions' 4 | export * from './value' 5 | export * from './variants' 6 | export * from './nuxt' 7 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // Global compile-time constants 4 | declare const __DEV__: boolean 5 | declare const __BROWSER__: boolean 6 | declare const __CI__: boolean 7 | -------------------------------------------------------------------------------- /src/types/nuxt.ts: -------------------------------------------------------------------------------- 1 | import type { MotionVariants } from './variants' 2 | 3 | export interface ModuleOptions { 4 | directives?: Record> 5 | excludePresets?: boolean 6 | } 7 | -------------------------------------------------------------------------------- /src/types/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { MotionVariants } from './variants' 2 | 3 | export interface MotionPluginOptions { 4 | directives?: Record> 5 | excludePresets?: boolean 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/utils/type-feature.ts: -------------------------------------------------------------------------------- 1 | export function objectEntries(obj: T) { 2 | return Object.entries(obj) as Array<[keyof T, T[keyof T]]> 3 | } 4 | 5 | export function objectKeys(obj: T) { 6 | return Object.keys(obj) as Array 7 | } 8 | -------------------------------------------------------------------------------- /playgrounds/vite/src/examples/basic.ts: -------------------------------------------------------------------------------- 1 | export default (x: string) => ` 2 |
14 | ` 15 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "https://motion.vueuse.js.org/*" 3 | to = "https://motion.vueuse.org/:splat" 4 | status = 301 5 | force = true 6 | 7 | [[redirects]] 8 | from = "https://vueuse-motion.netlify.app/*" 9 | to = "https://motion.vueuse.org/:splat" 10 | status = 301 11 | force = true 12 | -------------------------------------------------------------------------------- /playgrounds/nuxt/pages/test.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /playgrounds/vite-ssg/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ViteSSG } from 'vite-ssg/single-page' 2 | import { MotionPlugin } from '@vueuse/motion' 3 | import App from './App.vue' 4 | 5 | export const createApp = ViteSSG(App, ({ app }: any) => { 6 | app.use(MotionPlugin) 7 | 8 | if (import.meta.env.SSR) { 9 | // 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /playgrounds/vite/windi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite-plugin-windicss' 2 | 3 | defineConfig({ 4 | darkMode: false, // or 'media' or 'class' 5 | theme: { 6 | extend: { 7 | colors: { 8 | transparent: 'transparent', 9 | current: 'currentColor', 10 | }, 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /playgrounds/vite-ssg/vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | import type { defineComponent } from 'vue' 2 | 3 | declare module '*.vue' { 4 | const Component: ReturnType 5 | export default Component 6 | } 7 | 8 | declare module '*.md' { 9 | const Component: ReturnType 10 | export default Component 11 | } 12 | -------------------------------------------------------------------------------- /src/nuxt/src/runtime/templates/motion.ts: -------------------------------------------------------------------------------- 1 | import { MotionPlugin } from '@vueuse/motion' 2 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 3 | 4 | export default defineNuxtPlugin( 5 | (nuxtApp) => { 6 | const config = useRuntimeConfig() 7 | 8 | nuxtApp.vueApp.use(MotionPlugin, config.public.motion) 9 | }, 10 | ) 11 | -------------------------------------------------------------------------------- /src/types/value.ts: -------------------------------------------------------------------------------- 1 | export interface StopAnimation { 2 | stop: () => void 3 | } 4 | 5 | export type Transformer = (v: T) => T 6 | 7 | export type Subscriber = (v: T) => void 8 | 9 | export type PassiveEffect = (v: T, safeSetter: (v: T) => void) => void 10 | 11 | export type StartAnimation = (complete?: () => void) => StopAnimation 12 | -------------------------------------------------------------------------------- /playgrounds/vite-ssg/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite SSG 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "scripts": { 4 | "dev:docs": "nuxt dev", 5 | "build:docs": "nuxt generate", 6 | "preview:docs": "nuxt preview", 7 | "prepare:docs": "nuxt prepare" 8 | }, 9 | "dependencies": { 10 | "@vueuse/motion": "workspace:~" 11 | }, 12 | "devDependencies": { 13 | "@nuxt-themes/docus": "^1.15.0", 14 | "nuxt": "^3.11.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playgrounds/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "scripts": { 4 | "dev:nuxt": "nuxi dev", 5 | "build:nuxt": "nuxi build", 6 | "generate:nuxt": "nuxi generate", 7 | "prepare:nuxt": "nuxi prepare" 8 | }, 9 | "dependencies": { 10 | "@vueuse/motion": "workspace:~" 11 | }, 12 | "devDependencies": { 13 | "@nuxt/content": "^2.12.1", 14 | "nuxt": "^3.11.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playgrounds/vite/src/main.ts: -------------------------------------------------------------------------------- 1 | import { MotionPlugin } from '@vueuse/motion' 2 | import 'prism-theme-vars/base.css' 3 | import { createApp } from 'vue' 4 | import 'windi.css' 5 | import App from './App.vue' 6 | import Block from './components/Block.vue' 7 | import './index.css' 8 | 9 | const app = createApp(App) 10 | 11 | app.use(MotionPlugin) 12 | 13 | app.component('Block', Block) 14 | 15 | app.mount('#app') 16 | -------------------------------------------------------------------------------- /src/utils/events.ts: -------------------------------------------------------------------------------- 1 | const isBrowser = typeof window !== 'undefined' 2 | 3 | export const supportsPointerEvents = () => isBrowser && (window.onpointerdown === null || import.meta.env?.TEST) 4 | 5 | export const supportsTouchEvents = () => isBrowser && (window.ontouchstart === null || import.meta.env?.TEST) 6 | 7 | export const supportsMouseEvents = () => isBrowser && (window.onmousedown === null || import.meta.env?.TEST) 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | root: __dirname, 6 | define: { 7 | dev: JSON.stringify(false), 8 | }, 9 | test: { 10 | environment: 'happy-dom', 11 | include: ['./tests/**/*.spec.ts'], 12 | // Temporarily disable `transform` test 13 | exclude: ['./tests/transform.spec.ts'], 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /playgrounds/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Editor | @vueuse/motion 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/useReducedMotion.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import type { Ref } from 'vue' 3 | import { useMediaQuery } from '@vueuse/core' 4 | 5 | /** 6 | * Reactive prefers-reduced-motion. 7 | */ 8 | export function useReducedMotion(options: { window?: Window } = {}): Ref { 9 | const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)', options) 10 | 11 | return computed(() => reducedMotion.value) 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/reproduire.yml: -------------------------------------------------------------------------------- 1 | name: Reproduire 2 | on: 3 | issues: 4 | types: [labeled] 5 | 6 | permissions: 7 | issues: write 8 | 9 | jobs: 10 | reproduire: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 14 | - uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp 15 | with: 16 | label: needs reproduction 17 | -------------------------------------------------------------------------------- /playgrounds/vite-ssg/src/App.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 21 | 22 | 28 | -------------------------------------------------------------------------------- /src/features/syncVariants.ts: -------------------------------------------------------------------------------- 1 | import { watch } from 'vue' 2 | import type { MotionInstance, MotionVariants } from '../types' 3 | 4 | export function registerVariantsSync>({ state, apply }: MotionInstance) { 5 | // Watch for variant changes and apply the new one 6 | watch( 7 | state, 8 | (newVal) => { 9 | if (newVal) 10 | apply(newVal) 11 | }, 12 | { 13 | immediate: true, 14 | }, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/nuxt/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | externals: [ 5 | '@nuxt/kit', 6 | '@nuxt/schema', 7 | 'nuxt3', 8 | 'nuxt', 9 | 'vue', 10 | 'defu', 11 | '@vueuse/motion', 12 | 'csstype', 13 | '@vueuse/shared', 14 | 'framesync', 15 | 'style-value-types', 16 | '@vue/compiler-core', 17 | '@babel/parser', 18 | '@vue/shared', 19 | '@vueuse/core', 20 | ], 21 | }) 22 | -------------------------------------------------------------------------------- /playgrounds/vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'node:path' 3 | import vue from '@vitejs/plugin-vue' 4 | import { defineConfig } from 'vite' 5 | import WindiCSS from 'vite-plugin-windicss' 6 | 7 | export default defineConfig({ 8 | plugins: [vue(), WindiCSS()], 9 | resolve: { 10 | alias: [ 11 | { 12 | find: '@vueuse/motion', 13 | replacement: resolve(__dirname, '../../src/index.ts'), 14 | }, 15 | ], 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /docs/content/3.api/8.use-motion-features.md: -------------------------------------------------------------------------------- 1 | # useMotionFeatures 2 | 3 | useMotionFeatures is used to register features such as variant sync, event listeners. 4 | 5 | ## Parameters 6 | 7 | ### `instance` 8 | 9 | A motion instance from [useMotion](/api/use-motion) or [useMotions](/api/use-motions). 10 | 11 | ### `options` 12 | 13 | The same options object as [useMotion](/api/use-motion). 14 | 15 | ## Example 16 | 17 | [Example from the source](https://github.com/vueuse/motion/blob/main/src/useMotion.ts#L53) 18 | -------------------------------------------------------------------------------- /playgrounds/vite-ssg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "private": true, 4 | "scripts": { 5 | "dev:ssg": "vite", 6 | "build:ssg": "vite-ssg build", 7 | "preview:ssg": "vite preview" 8 | }, 9 | "dependencies": { 10 | "@vueuse/head": "^2.0.0", 11 | "@vueuse/motion": "workspace:~", 12 | "vue": "^3.4.27", 13 | "vue-router": "^4.3.2" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "^5.0.4", 17 | "vite": "5.2.12", 18 | "vite-ssg": "latest" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | 3 | export default defineNuxtConfig({ 4 | theme: '@nuxt-themes/docus', 5 | alias: { 6 | '@vueuse/motion': resolve(__dirname, '../src/index.ts'), 7 | '@vueuse/motion/nuxt': resolve(__dirname, '../src/nuxt/src/module.ts'), 8 | }, 9 | modules: ['@vueuse/motion/nuxt'], 10 | features: { 11 | devLogs: false, 12 | }, 13 | typescript: { 14 | includeWorkspace: true, 15 | }, 16 | pinceau: { 17 | followSymbolicLinks: false, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /playgrounds/vite-ssg/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | Vue({ 8 | include: [/\.vue$/, /\.md$/], 9 | }), 10 | ], 11 | ssgOptions: { 12 | script: 'async', 13 | }, 14 | resolve: { 15 | alias: [ 16 | { 17 | find: '@vueuse/motion', 18 | replacement: resolve(__dirname, '../../src/index.ts'), 19 | }, 20 | ], 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /src/usePermissiveTarget.ts: -------------------------------------------------------------------------------- 1 | import { watch } from 'vue' 2 | import { unrefElement } from '@vueuse/core' 3 | import type { MaybeRef } from 'vue' 4 | import type { PermissiveTarget } from './types' 5 | 6 | export function usePermissiveTarget(target: MaybeRef, onTarget: (target: HTMLElement | SVGElement) => void) { 7 | watch( 8 | () => unrefElement(target), 9 | (el) => { 10 | if (!el) 11 | return 12 | 13 | onTarget(el) 14 | }, 15 | { 16 | immediate: true, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /playgrounds/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "scripts": { 4 | "dev:vite": "vite", 5 | "build:vite": "vite build", 6 | "preview:vite": "vite preview" 7 | }, 8 | "dependencies": { 9 | "@vueuse/motion": "workspace:~", 10 | "prism-theme-vars": "^0.2.5", 11 | "prismjs": "^1.29.0", 12 | "vue": "^3.4.27" 13 | }, 14 | "devDependencies": { 15 | "@types/prismjs": "^1.26.4", 16 | "@vitejs/plugin-vue": "^5.0.4", 17 | "vite": "5.2.12", 18 | "vite-plugin-windicss": "^1.9.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu( 4 | { 5 | vue: true, 6 | typescript: true, 7 | markdown: false, 8 | rules: { 9 | 'semi': ['error', 'never'], 10 | 'jsdoc/check-param-names': 'off', 11 | 'jsdoc/check-tag-names': 'off', 12 | }, 13 | }, 14 | { 15 | ignores: [ 16 | '*.md', 17 | '*.css', 18 | 'dist/**/*', 19 | '.output/**/*', 20 | '.nuxt/**/*', 21 | 'coverage/', 22 | 'dist/', 23 | 'templates/', 24 | ], 25 | }, 26 | ) 27 | -------------------------------------------------------------------------------- /playgrounds/vite/src/examples/transitions.ts: -------------------------------------------------------------------------------- 1 | export default (visible: boolean) => ` 2 | 6 |
22 | 23 | ` 24 | -------------------------------------------------------------------------------- /src/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../playgrounds/nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "jsx": "preserve", 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "Node", 9 | 10 | "resolveJsonModule": true, 11 | "types": ["node", "vitest", "vitest/globals", "vite/client"], 12 | "allowJs": true, 13 | "strict": true, 14 | "noEmit": true, 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "skipLibCheck": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playgrounds/nuxt/components/content/Block.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /docs/content/2.features/0.presets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Presets 3 | description: Presets are a set of animations that are bundled with the package. 4 | toc: false 5 | --- 6 | 7 | It allows you to start implementing animations straight after installing the plugin in your Vue app. 8 | 9 | You can add your own presets to your app using [Custom Directives](/features/directive-usage#custom-directives). 10 | 11 | If you have any ideas for new presets add an issue on [GitHub](https://github.com/vueuse/motion#issues), or reach me on [Twitter](https://twitter.com/yaeeelglx). 12 | 13 | 14 | -------------------------------------------------------------------------------- /playgrounds/vite/src/demos/Sandbox.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | # Restore all git changes 6 | git restore --source=HEAD --staged --worktree -- package.json pnpm-lock.yaml 7 | 8 | # Resolve pnpm 9 | pnpm i --frozen-lockfile=false 10 | 11 | # Update token 12 | if [[ ! -z ${NPM_TOKEN} ]] ; then 13 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc 14 | echo "registry=https://registry.npmjs.org/" >> ~/.npmrc 15 | echo "always-auth=true" >> ~/.npmrc 16 | npm whoami 17 | fi 18 | 19 | # Release package 20 | echo "⚡ Publishing with tag latest" 21 | npx npm@8.17.0 publish --tag latest --access public --tolerate-republish 22 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | docus: { 3 | title: '@vueuse/motion', 4 | description: '🤹 Vue Composables putting your components in motion', 5 | image: 'https://motion.vueuse.org/banner.png', 6 | socials: { 7 | twitter: 'yaeeelglx', 8 | github: 'vueuse/motion', 9 | }, 10 | aside: { 11 | level: 1, 12 | }, 13 | header: { 14 | title: '@vueuse/motion', 15 | }, 16 | footer: { 17 | credits: { 18 | icon: 'IconDocus', 19 | text: 'Powered by Docus', 20 | href: 'https://docus.dev', 21 | }, 22 | }, 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /src/presets/fade.ts: -------------------------------------------------------------------------------- 1 | import type { MotionVariants } from '../types' 2 | 3 | export const fade: MotionVariants = { 4 | initial: { 5 | opacity: 0, 6 | }, 7 | enter: { 8 | opacity: 1, 9 | }, 10 | } 11 | 12 | export const fadeVisible: MotionVariants = { 13 | initial: { 14 | opacity: 0, 15 | }, 16 | visible: { 17 | opacity: 1, 18 | }, 19 | } 20 | 21 | export const fadeVisibleOnce: MotionVariants = { 22 | initial: { 23 | opacity: 0, 24 | }, 25 | visibleOnce: { 26 | opacity: 1, 27 | }, 28 | } 29 | 30 | export default { 31 | fade, 32 | fadeVisible, 33 | fadeVisibleOnce, 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | -------------------------------------------------------------------------------- /src/nuxt/src/runtime/composables/index.ts: -------------------------------------------------------------------------------- 1 | export { reactiveStyle } from '@vueuse/motion' 2 | export { reactiveTransform } from '@vueuse/motion' 3 | export { useElementStyle } from '@vueuse/motion' 4 | export { useElementTransform } from '@vueuse/motion' 5 | export { useMotion } from '@vueuse/motion' 6 | export { useMotionControls } from '@vueuse/motion' 7 | export { useMotionProperties } from '@vueuse/motion' 8 | export { useMotions } from '@vueuse/motion' 9 | export { useMotionTransitions } from '@vueuse/motion' 10 | export { useMotionVariants } from '@vueuse/motion' 11 | export { useSpring } from '@vueuse/motion' 12 | export { useReducedMotion } from '@vueuse/motion' 13 | -------------------------------------------------------------------------------- /playgrounds/vite/src/examples/delay.ts: -------------------------------------------------------------------------------- 1 | export default () => ` 2 |
16 | [ ... ] 17 |
32 | 33 | ` 34 | -------------------------------------------------------------------------------- /scripts/release-nightly.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | # Restore all git changes 6 | git restore --source=HEAD --staged --worktree -- package.json pnpm-lock.yaml 7 | 8 | # Bump versions to edge 9 | pnpm jiti ./scripts/bump-nightly 10 | 11 | # Resolve pnpm 12 | pnpm i --frozen-lockfile=false 13 | 14 | # Update token 15 | if [[ ! -z ${NPM_TOKEN} ]] ; then 16 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc 17 | echo "registry=https://registry.npmjs.org/" >> ~/.npmrc 18 | echo "always-auth=true" >> ~/.npmrc 19 | npm whoami 20 | fi 21 | 22 | # Release package 23 | echo "⚡ Publishing nightly version" 24 | npx npm@8.17.0 publish --access public --tolerate-republish 25 | -------------------------------------------------------------------------------- /src/utils/is-motion-instance.ts: -------------------------------------------------------------------------------- 1 | import { isRef } from 'vue' 2 | import type { MotionInstance, MotionVariants } from '../types' 3 | 4 | /** 5 | * Check whether an object is a Motion Instance or not. 6 | * 7 | * Can be useful while building packages based on @vueuse/motion. 8 | * 9 | * @param obj 10 | * @returns bool 11 | */ 12 | export function isMotionInstance>(obj: any): obj is MotionInstance { 13 | const _obj = obj as MotionInstance 14 | 15 | return _obj.apply !== undefined && typeof _obj.apply === 'function' && _obj.set !== undefined && typeof _obj.set === 'function' && _obj.target !== undefined && isRef(_obj.target) 16 | } 17 | -------------------------------------------------------------------------------- /src/presets/pop.ts: -------------------------------------------------------------------------------- 1 | import type { MotionVariants } from '../types' 2 | 3 | export const pop: MotionVariants = { 4 | initial: { 5 | scale: 0, 6 | opacity: 0, 7 | }, 8 | enter: { 9 | scale: 1, 10 | opacity: 1, 11 | }, 12 | } 13 | 14 | export const popVisible: MotionVariants = { 15 | initial: { 16 | scale: 0, 17 | opacity: 0, 18 | }, 19 | visible: { 20 | scale: 1, 21 | opacity: 1, 22 | }, 23 | } 24 | 25 | export const popVisibleOnce: MotionVariants = { 26 | initial: { 27 | scale: 0, 28 | opacity: 0, 29 | }, 30 | visibleOnce: { 31 | scale: 1, 32 | opacity: 1, 33 | }, 34 | } 35 | 36 | export default { 37 | pop, 38 | popVisible, 39 | popVisibleOnce, 40 | } 41 | -------------------------------------------------------------------------------- /src/presets/index.ts: -------------------------------------------------------------------------------- 1 | export { fade, fadeVisible, fadeVisibleOnce } from './fade' 2 | export { pop, popVisible, popVisibleOnce } from './pop' 3 | export { 4 | rollBottom, 5 | rollLeft, 6 | rollRight, 7 | rollTop, 8 | rollVisibleBottom, 9 | rollVisibleLeft, 10 | rollVisibleRight, 11 | rollVisibleTop, 12 | rollVisibleOnceBottom, 13 | rollVisibleOnceLeft, 14 | rollVisibleOnceRight, 15 | rollVisibleOnceTop, 16 | } from './roll' 17 | export { 18 | slideBottom, 19 | slideLeft, 20 | slideRight, 21 | slideTop, 22 | slideVisibleBottom, 23 | slideVisibleLeft, 24 | slideVisibleRight, 25 | slideVisibleTop, 26 | slideVisibleOnceBottom, 27 | slideVisibleOnceLeft, 28 | slideVisibleOnceRight, 29 | slideVisibleOnceTop, 30 | } from './slide' 31 | -------------------------------------------------------------------------------- /scripts/bump-nightly.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { promises as fsp } from 'node:fs' 3 | import { execSync } from 'node:child_process' 4 | import { resolve } from 'pathe' 5 | 6 | async function main() { 7 | const commit = execSync('git rev-parse --short HEAD').toString('utf-8').trim() 8 | const date = Math.round(Date.now() / (1000 * 60)) 9 | 10 | const pkgPath = resolve(process.cwd(), 'package.json') 11 | 12 | const pkg = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}')) 13 | 14 | pkg.version = `${pkg.version}-${date}.${commit}` 15 | 16 | pkg.name = `vueuse-motion-nightly` 17 | await fsp.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`) 18 | } 19 | 20 | main().catch((err) => { 21 | console.error(err) 22 | process.exit(1) 23 | }) 24 | -------------------------------------------------------------------------------- /src/useMotionVariants.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRef, Ref } from 'vue' 2 | import { computed, ref, unref } from 'vue' 3 | import type { MotionVariants, Variant } from './types' 4 | 5 | /** 6 | * A Composable handling variants selection and features. 7 | */ 8 | export function useMotionVariants>(variants: MaybeRef = {} as MaybeRef) { 9 | // Unref variants 10 | const _variants = unref(variants) as V 11 | 12 | // Current variant string 13 | const variant = ref() as Ref 14 | 15 | // Current variant state 16 | const state = computed(() => { 17 | if (!variant.value) 18 | return 19 | 20 | return _variants[variant.value] 21 | }) 22 | 23 | return { 24 | state, 25 | variant, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/element.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import type { VueInstance } from '@vueuse/core' 3 | import type { MaybeRef, Ref } from 'vue' 4 | import type { MotionTarget, PermissiveTarget } from '../types' 5 | 6 | export function resolveElement(target: MaybeRef): Ref { 7 | const _targetRef = ref(target) 8 | const targetRef = ref() 9 | 10 | watch( 11 | _targetRef, 12 | (newVal) => { 13 | if (!newVal) 14 | return 15 | 16 | if ((newVal as VueInstance).$el) { 17 | targetRef.value = (newVal as VueInstance).$el as MotionTarget 18 | return 19 | } 20 | 21 | // $el does not exist, it might then be a correct DOM element 22 | targetRef.value = newVal 23 | }, 24 | { immediate: true }, 25 | ) 26 | 27 | return targetRef 28 | } 29 | -------------------------------------------------------------------------------- /docs/content/3.api/5.use-motion-variants.md: -------------------------------------------------------------------------------- 1 | # useMotionVariants 2 | 3 | useMotionVariants is used to handle the [Variants](/features/variants) state and selection. 4 | 5 | ## Parameters 6 | 7 | ### `variants` 8 | 9 | A [Variants](/features/variants#custom-variants) definition. 10 | 11 | ## Exposed 12 | 13 | ### `state` 14 | 15 | The current variant data value as a computed. 16 | 17 | ### `variant` 18 | 19 | A string reference that updates the state when changed. 20 | 21 | #### Example 22 | 23 | ```typescript 24 | const variants: MotionVariants = { 25 | initial: { 26 | opacity: 0, 27 | y: 100, 28 | }, 29 | enter: { 30 | opacity: 1, 31 | y: 0, 32 | }, 33 | } 34 | 35 | const { variant, state } = useMotionVariants(variants) 36 | 37 | variant.value = 'initial' 38 | 39 | nextTick(() => (variant.value = 'enter')) 40 | ``` 41 | -------------------------------------------------------------------------------- /src/utils/subscription-manager.ts: -------------------------------------------------------------------------------- 1 | type GenericHandler = (...args: any) => void 2 | 3 | /** 4 | * A generic subscription manager. 5 | */ 6 | export class SubscriptionManager { 7 | private subscriptions = new Set() 8 | 9 | add(handler: Handler) { 10 | this.subscriptions.add(handler) 11 | return () => this.subscriptions.delete(handler) 12 | } 13 | 14 | notify( 15 | /** 16 | * Using ...args would be preferable but it's array creation and this 17 | * might be fired every frame. 18 | */ 19 | a?: Parameters[0], 20 | b?: Parameters[1], 21 | c?: Parameters[2], 22 | ) { 23 | if (!this.subscriptions.size) 24 | return 25 | for (const handler of this.subscriptions) handler(a, b, c) 26 | } 27 | 28 | clear() { 29 | this.subscriptions.clear() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Motion.ts: -------------------------------------------------------------------------------- 1 | import type { Component, PropType } from 'vue' 2 | 3 | import { defineComponent, h, useSlots } from 'vue' 4 | import { variantToStyle } from '../utils/transform' 5 | import { MotionComponentProps, setupMotionComponent } from '../utils/component' 6 | 7 | export default defineComponent({ 8 | name: 'Motion', 9 | props: { 10 | ...MotionComponentProps, 11 | is: { 12 | type: [String, Object] as PropType, 13 | default: 'div', 14 | }, 15 | }, 16 | setup(props) { 17 | const slots = useSlots() 18 | 19 | const { motionConfig, setNodeInstance } = setupMotionComponent(props) 20 | 21 | return () => { 22 | const style = variantToStyle(motionConfig.value.initial || {}) 23 | const node = h(props.is, undefined, slots) 24 | 25 | setNodeInstance(node, 0, style) 26 | 27 | return node 28 | } 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /tests/reactiveStyle.spec.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | import { describe, expect, it } from 'vitest' 3 | import { reactiveStyle } from '../src/reactiveStyle' 4 | 5 | describe('reactiveStyle', () => { 6 | it('create a style object from props', () => { 7 | const { style } = reactiveStyle({ 8 | backgroundColor: 'blue', 9 | }) 10 | 11 | expect(style.value.backgroundColor).toBe('blue') 12 | }) 13 | 14 | it('create a reactive style object from props', () => { 15 | const { state, style } = reactiveStyle() 16 | 17 | state.backgroundColor = 'blue' 18 | 19 | nextTick(() => expect(style.value.backgroundColor).toBe('blue')) 20 | }) 21 | 22 | it('add default units for style properties', () => { 23 | const { state, style } = reactiveStyle() 24 | 25 | state.marginTop = 10 26 | 27 | nextTick(() => expect(style.value.marginTop).toBe('10px')) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /docs/content/3.api/3.use-motions.md: -------------------------------------------------------------------------------- 1 | # useMotions 2 | 3 | useMotions is used to access the motion instances from v-motion directives declared from templates. 4 | 5 | If you declare a name using `v-motion` attribute value, the motion instances will be added to the global useMotions state and be accessible from any component. 6 | 7 | Be careful about duplicating the same name, note that the name can be including a variable. 8 | 9 | ## Exposed 10 | 11 | ### `{ ...motionControls }` 12 | 13 | useMotions exposes an object in which keys are defined from all the **v-motion** for which you defined a name value. 14 | 15 | Each values are [Motion Instances](/features/motion-instance) for the named elements. 16 | 17 | ## Example 18 | 19 | ```vue 20 | 23 | 24 | 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@vueuse/motion' 3 | navigation: false 4 | layout: page 5 | main: 6 | fluid: false 7 | --- 8 | 9 | :ellipsis{right=0px width=75% blur=150px} 10 | 11 | ::block-hero 12 | --- 13 | cta: 14 | - Get started 15 | - /getting-started/introduction 16 | secondary: 17 | - Open on GitHub → 18 | - https://github.com/vueuse/motion 19 | --- 20 | 21 | #title 22 | @vueuse/motion 23 | 24 | #description 25 | Composables putting your components in motion. 26 | 27 | #extra 28 | ::list 29 | - 🏎 **Smooth animations** based on [Popmotion](https://popmotion.io/) 30 | - 🎮 **Declarative** API inspired by [Framer Motion](https://www.framer.com/motion/) 31 | - 🚀 **Plug** & **play** with **20+ presets** 32 | - 🌐 **SSR Ready** 33 | - 🚚 First-class support for **Nuxt 3** 34 | - ✨ Written in **TypeScript** 35 | - 🏋️‍♀️ Lightweight with **<20kb** bundle size 36 | :: 37 | 38 | #support 39 | :person 40 | :: 41 | -------------------------------------------------------------------------------- /.github/workflows/reproduire-close.yml: -------------------------------------------------------------------------------- 1 | name: Close incomplete issues 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '30 1 * * *' # run every day 6 | 7 | permissions: 8 | issues: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 15 | with: 16 | days-before-stale: -1 # Issues and PR will never be flagged stale automatically. 17 | stale-issue-label: needs reproduction # Label that flags an issue as stale. 18 | only-labels: needs reproduction # Only process these issues 19 | days-before-issue-close: 7 20 | ignore-updates: true 21 | remove-stale-when-updated: false 22 | close-issue-message: This issue was closed because it was open for 7 days without a reproduction. 23 | close-issue-label: closed by bot 24 | operations-per-run: 300 # default 30 25 | -------------------------------------------------------------------------------- /docs/components/content/examples/MotionComponent.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 45 | -------------------------------------------------------------------------------- /tests/isMotionInstance.spec.ts: -------------------------------------------------------------------------------- 1 | import { ref } from '@vue/reactivity' 2 | import { mount } from '@vue/test-utils' 3 | import { describe, expect, it } from 'vitest' 4 | import { isMotionInstance, useMotion } from '../src' 5 | 6 | const TestComponent = { 7 | template: '
Hello world
', 8 | } 9 | 10 | function getElementRef() { 11 | const c = mount(TestComponent) 12 | 13 | return ref(c.element as HTMLElement) 14 | } 15 | 16 | describe('isMotionInstance', () => { 17 | it('recognize a motion instance', () => { 18 | const ref = getElementRef() 19 | 20 | const motionInstance = useMotion(ref) 21 | 22 | expect(isMotionInstance(motionInstance)).toBe(true) 23 | }) 24 | 25 | it('does not recognize wrong object', () => { 26 | const motionInstance = { 27 | set: 'test', 28 | stopTransitions: {}, 29 | target: '', 30 | apply: 25, 31 | } 32 | 33 | expect(isMotionInstance(motionInstance)).toBe(false) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/features/visibilityHooks.ts: -------------------------------------------------------------------------------- 1 | import { useIntersectionObserver } from '@vueuse/core' 2 | import { unref } from 'vue' 3 | import type { MotionInstance, MotionVariants } from '../types' 4 | 5 | export function registerVisibilityHooks>({ target, variants, variant }: MotionInstance) { 6 | const _variants = unref(variants) 7 | 8 | // Bind intersection observer on target 9 | if (_variants && (_variants.visible || _variants.visibleOnce)) { 10 | useIntersectionObserver(target, ([{ isIntersecting }]) => { 11 | if (_variants.visible) { 12 | if (isIntersecting) 13 | variant.value = 'visible' 14 | else variant.value = 'initial' 15 | } 16 | else if (_variants.visibleOnce) { 17 | if (isIntersecting && variant.value !== 'visibleOnce') 18 | variant.value = 'visibleOnce' 19 | else if (!variant.value) 20 | variant.value = 'initial' 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/content/3.api/12.reactive-transform.md: -------------------------------------------------------------------------------- 1 | # reactiveTransform 2 | 3 | reactiveTransform is an helper function creating a reactive object compatible with an HTML `style` attribute. 4 | 5 | ## Parameters 6 | 7 | ### `props` 8 | 9 | Default [TransformProperties](https://github.com/vueuse/motion/tree/main/src/types/variants.ts#L21) object to create the reactive one from. 10 | 11 | ## Exposed 12 | 13 | ### `state` 14 | 15 | The reactive [TransformProperties](https://github.com/vueuse/motion/tree/main/src/types/variants.ts#L21) object to manipulate. 16 | 17 | ### `style` 18 | 19 | A reactive [transform attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/transform) compatible string. 20 | 21 | #### Example 22 | 23 | ```vue 24 | 27 | 28 | 39 | ``` 40 | -------------------------------------------------------------------------------- /tests/useMotion.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { ref } from 'vue' 3 | import { describe, expect, it } from 'vitest' 4 | import { useMotion } from '../src' 5 | 6 | const TestComponent = { 7 | template: '
Hello world
', 8 | } 9 | 10 | function getElementRef() { 11 | const c = mount(TestComponent) 12 | 13 | return ref(c.element as HTMLElement) 14 | } 15 | 16 | describe('useMotion', () => { 17 | it('accepts an element', () => { 18 | const element = getElementRef() 19 | 20 | const { target, variant, variants, state, apply, stop, isAnimating, leave, motionProperties, set } = useMotion(element) 21 | 22 | expect(target).toBeDefined() 23 | expect(variant).toBeDefined() 24 | expect(variants).toBeDefined() 25 | expect(state).toBeDefined() 26 | expect(apply).toBeDefined() 27 | expect(stop).toBeDefined() 28 | expect(isAnimating).toBeDefined() 29 | expect(leave).toBeDefined() 30 | expect(motionProperties).toBeDefined() 31 | expect(set).toBeDefined() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /docs/content/3.api/9.use-element-style.md: -------------------------------------------------------------------------------- 1 | # useElementStyle 2 | 3 | useElementStyle is used to sync a reactive object to a target element CSS styling. 4 | 5 | It uses [reactiveStyle](https://github.com/vueuse/motion/blob/main/src/reactiveStyle.ts) and bind it to a target. 6 | 7 | ## Parameters 8 | 9 | ### `target` 10 | 11 | Target must be an element (**SVG** / **HTML**), or a reference to an element. 12 | 13 | If the target reference is updated, the current style will be updated from the new element styling. 14 | 15 | ## Exposed 16 | 17 | ### `style` 18 | 19 | Style is the current `target` [Style Properties](/features/motion-properties#style-properties) as a reactive object. 20 | 21 | When you change a value, it will update the element style property accordingly. 22 | 23 | ### `stop()` 24 | 25 | Stop function will stop the watcher that syncs the element style with the reactive object. 26 | 27 | #### Example 28 | 29 | ```typescript 30 | const target = ref() 31 | 32 | const { style, stop } = useElementStyle(target) 33 | 34 | style.opacity = 0 35 | ``` 36 | -------------------------------------------------------------------------------- /.github/workflows/targets.yml: -------------------------------------------------------------------------------- 1 | name: Build playgrounds 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] # macos-latest, windows-latest 18 | node: [20] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | fetch-depth: 0 26 | 27 | - name: Enable corepack 28 | run: npm i -fg corepack && corepack enable 29 | 30 | - name: Setup node 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node }} 34 | cache: pnpm 35 | 36 | - name: Install dependencies 37 | run: pnpm install 38 | 39 | - name: Build project (Vite) 40 | run: pnpm build:vite 41 | 42 | - name: Build project (Nuxt) 43 | run: pnpm build:nuxt 44 | 45 | - name: Build project (Vite SSG) 46 | run: pnpm build:ssg 47 | -------------------------------------------------------------------------------- /docs/content/3.api/10.use-element-transform.md: -------------------------------------------------------------------------------- 1 | # useElementTransform 2 | 3 | useElementTransform is used to sync a reactive object to a target element CSS transform. 4 | 5 | It uses [reactiveTransform](https://github.com/vueuse/motion/blob/main/src/reactiveTransform.ts) and binds it to a target. 6 | 7 | ## Parameters 8 | 9 | ### `target` 10 | 11 | Target must be an element (**SVG** / **HTML**), or a reference to an element. 12 | 13 | If the target reference is updated, the current transform will be updated from the new element styling. 14 | 15 | ## Exposed 16 | 17 | ### `transform` 18 | 19 | Transform is the current `target` [Transform Properties](/features/motion-properties#transform-properties) as a reactive object. 20 | 21 | When you change a value, it will update the element transform property accordingly. 22 | 23 | ### `stop()` 24 | 25 | Stop function will stop the watcher that syncs the element transform with the reactive object. 26 | 27 | ## Example 28 | 29 | ```typescript 30 | const target = ref() 31 | 32 | const { transform, stop } = useElementTransform(target) 33 | 34 | transform.scale = 1.2 35 | ``` 36 | -------------------------------------------------------------------------------- /src/features/lifeCycleHooks.ts: -------------------------------------------------------------------------------- 1 | import { unref, watch } from 'vue' 2 | import type { MotionInstance, MotionVariants } from '../types' 3 | 4 | export function registerLifeCycleHooks>({ set, target, variants, variant }: MotionInstance) { 5 | const _variants = unref(variants) 6 | 7 | watch( 8 | () => target, 9 | () => { 10 | // Cancel cycle if no variants 11 | if (!_variants) 12 | return 13 | 14 | // Set initial before the element is mounted 15 | if (_variants.initial) { 16 | // Set initial variant properties immediately, skipping transitions 17 | set('initial') 18 | 19 | // Set variant to sync `state` which is used to undo event variant transitions 20 | // NOTE: This triggers an (instant) animation even though properties have already been applied 21 | variant.value = 'initial' 22 | } 23 | 24 | // Lifecycle hooks bindings 25 | if (_variants.enter) 26 | variant.value = 'enter' 27 | }, 28 | { 29 | immediate: true, 30 | flush: 'pre', 31 | }, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/slugify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a string to a slug. 3 | * 4 | * Source: https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1 5 | * Credits: @hagemann 6 | * 7 | * Edited to transform camel naming to slug with `-`. 8 | */ 9 | export function slugify(str: string) { 10 | const a = 'àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;' 11 | const b = 'aaaaaaaaaacccddeeeeeeeegghiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------' 12 | const p = new RegExp(a.split('').join('|'), 'g') 13 | 14 | return str 15 | .toString() 16 | .replace(/[A-Z]/g, s => `-${s}`) // Camel to slug 17 | .toLowerCase() 18 | .replace(/\s+/g, '-') // Replace spaces with - 19 | .replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters 20 | .replace(/&/g, '-and-') // Replace & with 'and' 21 | .replace(/[^\w\-]+/g, '') // Remove all non-word characters 22 | .replace(/-{2,}/g, '-') // Replace multiple - with single - 23 | .replace(/^-+/, '') // Trim - from start of text 24 | .replace(/-+$/, '') // Trim - from end of text 25 | } 26 | 27 | export default slugify 28 | -------------------------------------------------------------------------------- /docs/content/3.api/6.use-motion-transitions.md: -------------------------------------------------------------------------------- 1 | # useMotionTransitions 2 | 3 | useMotionTransitions is used to handle the multiple animations created when you animate your elements. 4 | 5 | It exposes `push` and `stop` which are both functions. 6 | 7 | ## Exposed 8 | 9 | ### `push(key, value, target, transition)` 10 | 11 | Push function run and add a transition to the current useMotionTransitions instance. 12 | 13 | ### `stop(keys | key | undefined)` 14 | 15 | Stop is a function that lets you stop ongoing animations for a specific element. 16 | 17 | Calling it without argument will be stopping all the animations. 18 | 19 | Calling it with an array of [Motion Properties](/features/motion-properties) keys will stop every specified key. 20 | 21 | Calling it with a single motion property key will stop the specified key. 22 | 23 | ## Example 24 | 25 | ```typescript 26 | const target = ref() 27 | 28 | const { motionProperties } = useMotionProperties(target) 29 | 30 | motionProperties.x = 0 31 | 32 | const { push, stop } = useMotionTransitions() 33 | 34 | push('x', 100, motionProperties, { type: 'spring', bounce: 4 }) 35 | 36 | setTimeout(stop, 4000) 37 | ``` 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Yaël GUILLOUX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/content/3.api/11.reactive-style.md: -------------------------------------------------------------------------------- 1 | # reactiveStyle 2 | 3 | reactiveStyle is an helper function creating a reactive object compatible with an HTML `style` attribute. 4 | 5 | ## Parameters 6 | 7 | ### `props` 8 | 9 | Default [StyleProperties](https://github.com/vueuse/motion/tree/main/src/types/variants.ts#L49-L50) object to create the reactive one from. 10 | 11 | ## Exposed 12 | 13 | ### `state` 14 | 15 | The reactive [StyleProperties](https://github.com/vueuse/motion/tree/main/src/types/variants.ts#L49-L50) object to manipulate. 16 | 17 | ### `style` 18 | 19 | A reactive [style attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style) compatible string. 20 | 21 | #### Example 22 | 23 | ```vue 24 | 29 | 30 | 43 | ``` 44 | -------------------------------------------------------------------------------- /tests/useMotionTransitions.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { useMotionTransitions } from '../src' 3 | import type { Transition } from '../src/types/transitions' 4 | 5 | const defaultTransition: Transition = { 6 | type: 'spring', 7 | stiffness: 500, 8 | damping: 25, 9 | restDelta: 0.5, 10 | restSpeed: 10, 11 | } 12 | 13 | describe('useMotionTransitions', () => { 14 | it('creates a motion value', () => { 15 | const { push, motionValues } = useMotionTransitions() 16 | 17 | push('x', 0, { x: 25 }, defaultTransition) 18 | 19 | expect(Object.values(motionValues.value).length).toBe(1) 20 | }) 21 | 22 | it('clears motion values on stop', async () => { 23 | const { push, motionValues, stop } = useMotionTransitions() 24 | 25 | push('x', 0, { x: 25 }, defaultTransition) 26 | push('y', 0, { y: 25 }, defaultTransition) 27 | push('opacity', 0, { opacity: 1 }, defaultTransition) 28 | push('height', 0, { height: 25 }, defaultTransition) 29 | 30 | expect(Object.values(motionValues.value).length).toBe(4) 31 | 32 | stop() 33 | 34 | expect(Object.values(motionValues.value).length).toBe(0) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Directive 2 | export { directive as MotionDirective } from './directive' 3 | 4 | // Plugin 5 | export { MotionPlugin } from './plugin' 6 | 7 | // Utils 8 | export { slugify } from './utils/slugify' 9 | export { isMotionInstance } from './utils/is-motion-instance' 10 | 11 | // Presets 12 | export * from './presets' 13 | 14 | // Typings 15 | export * from './types' 16 | 17 | // Components 18 | export * from './components' 19 | 20 | // Composables 21 | export { reactiveStyle } from './reactiveStyle' 22 | export { reactiveTransform } from './reactiveTransform' 23 | export { useElementStyle } from './useElementStyle' 24 | export { useElementTransform } from './useElementTransform' 25 | export { useMotion } from './useMotion' 26 | export { useMotionControls } from './useMotionControls' 27 | export { useMotionFeatures } from './useMotionFeatures' 28 | export { useMotionProperties } from './useMotionProperties' 29 | export { useMotions } from './useMotions' 30 | export { useMotionTransitions } from './useMotionTransitions' 31 | export { useMotionVariants } from './useMotionVariants' 32 | export { useSpring } from './useSpring' 33 | export { useReducedMotion } from './useReducedMotion' 34 | -------------------------------------------------------------------------------- /playgrounds/vite/src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | @apply h-full; 3 | } 4 | 5 | body { 6 | @apply text-white flex items-center justify-center bg-gradient-to-b from-violet-600 to-violet-900 bg-cover bg-fixed overflow-y-scroll; 7 | } 8 | 9 | #app { 10 | @apply w-full md:w-8/12 xl:w-1/2 p-6; 11 | } 12 | 13 | /* PrismJS theme fix */ 14 | pre[class*='language-'] { 15 | margin: 0; 16 | border-radius: 0; 17 | box-shadow: none; 18 | } 19 | 20 | :root { 21 | --prism-scheme: dark; 22 | --prism-foreground: #d4cfbf; 23 | --prism-background: theme('colors.gray.800'); 24 | --prism-comment: #758575; 25 | --prism-string: #ce9178; 26 | --prism-literal: #4fb09d; 27 | --prism-keyword: #4d9375; 28 | --prism-function: #c2c275; 29 | --prism-deleted: #a14f55; 30 | --prism-class: #5ebaa8; 31 | --prism-builtin: #cb7676; 32 | --prism-property: #dd8e6e; 33 | --prism-namespace: #c96880; 34 | --prism-punctuation: #d4d4d4; 35 | --prism-decorator: #bd8f8f; 36 | --prism-regex: #ab5e3f; 37 | --prism-json-property: #6b8b9e; 38 | --prism-line-number: #888888; 39 | --prism-line-number-gutter: #eeeeee; 40 | --prism-line-highlight-background: #444444; 41 | --prism-selection-background: #444444; 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/nightly-release.yml: -------------------------------------------------------------------------------- 1 | name: nightly release 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | tags: 7 | - '!**' 8 | pull_request: 9 | types: [opened, synchronize, labeled, ready_for_review] 10 | branches: [main] 11 | paths-ignore: 12 | - 'docs/**' 13 | - 'playgrounds/**' 14 | 15 | jobs: 16 | release: 17 | if: ${{ !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'cr-tracked') }} 18 | 19 | strategy: 20 | matrix: 21 | node: [20] 22 | os: [ubuntu-latest] 23 | 24 | runs-on: ${{ matrix.os }} 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | 30 | - name: Enable corepack 31 | run: npm i -fg corepack && corepack enable 32 | 33 | - name: Setup node 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node }} 37 | cache: pnpm 38 | 39 | - name: Install dependencies 40 | run: pnpm install --frozen-lockfile 41 | 42 | - name: Build 43 | run: pnpm build 44 | 45 | - name: Release with pkg-pr-new 46 | run: pnpx pkg-pr-new publish --compact --pnpm 47 | -------------------------------------------------------------------------------- /docs/content/2.features/2.composable-usage.md: -------------------------------------------------------------------------------- 1 | # Composable Usage 2 | 3 | vueuse/motion is written using Composition API. 4 | 5 | The composable usage of this package allows you to create animations from the `setup` hook of your components. 6 | 7 | ## Your first useMotion 8 | 9 | useMotion is the core composable of this package. 10 | 11 | It is a function that takes three parameters. 12 | 13 | The first parameter is the `target`. 14 | 15 | The target can be HTML or SVG elements, or references to these types. 16 | 17 | The second parameter are the `variants`. 18 | 19 | The [Variants Definitions](/features/variants) are described in a specific page. 20 | 21 | ```vue 22 | 38 | ``` 39 | 40 | Once called, the useMotion composable will return an instance of [Motion Instance](/features/motion-instance). 41 | 42 | By using this motion instance members, you will be able to animate the element with ease. 43 | -------------------------------------------------------------------------------- /tests/useElementStyle.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { nextTick, ref } from 'vue' 3 | import { describe, expect, it } from 'vitest' 4 | import { useElementStyle } from '../src' 5 | 6 | const TestComponent = { 7 | template: '
Hello world
', 8 | } 9 | 10 | function getElementRef() { 11 | const c = mount(TestComponent) 12 | 13 | return ref(c.element as HTMLElement) 14 | } 15 | 16 | describe('useElementStyle', () => { 17 | it('accepts an element', () => { 18 | const element = getElementRef() 19 | 20 | const { style } = useElementStyle(element) 21 | 22 | expect(style).toBeDefined() 23 | }) 24 | 25 | it('mutates style properties', () => { 26 | const element = getElementRef() 27 | 28 | const { style } = useElementStyle(element) 29 | 30 | style.backgroundColor = 'blue' 31 | 32 | expect(style.backgroundColor).toBe('blue') 33 | }) 34 | 35 | it('mutates element properties', async () => { 36 | const element = getElementRef() 37 | 38 | const { style } = useElementStyle(element) 39 | 40 | style.backgroundColor = 'blue' 41 | 42 | await nextTick() 43 | 44 | expect(element.value.style.backgroundColor).toBe('blue') 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /playgrounds/nuxt/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | 3 | export default defineNuxtConfig({ 4 | features: { 5 | devLogs: false, 6 | }, 7 | css: ['~/assets/pico.css'], 8 | alias: { 9 | '@vueuse/motion': resolve(__dirname, '../../src/index.ts'), 10 | '@vueuse/motion/nuxt': resolve(__dirname, '../../src/nuxt/src/module.ts'), 11 | }, 12 | modules: ['@vueuse/motion/nuxt', '@nuxt/content'], 13 | content: { 14 | highlight: { 15 | theme: 'one-dark-pro', 16 | preload: ['json', 'js', 'ts', 'html', 'css', 'vue'], 17 | }, 18 | }, 19 | components: { 20 | dirs: [ 21 | { 22 | path: './components', 23 | global: true, 24 | }, 25 | ], 26 | }, 27 | runtimeConfig: { 28 | public: { 29 | motion: { 30 | directives: { 31 | 'pop-bottom': { 32 | initial: { 33 | scale: 0, 34 | opacity: 0, 35 | y: 100, 36 | }, 37 | visible: { 38 | scale: 1, 39 | opacity: 1, 40 | y: 0, 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | typescript: { 48 | includeWorkspace: true, 49 | }, 50 | }) 51 | -------------------------------------------------------------------------------- /src/useMotionTransitions.ts: -------------------------------------------------------------------------------- 1 | import type { MotionProperties, MotionTransitions, ResolvedValueTarget, Transition } from './types' 2 | import { useMotionValues } from './useMotionValues' 3 | import { getAnimation } from './utils/transition' 4 | 5 | /** 6 | * A Composable holding all the ongoing transitions in a local reference. 7 | */ 8 | export function useMotionTransitions(): MotionTransitions { 9 | const { motionValues, stop, get } = useMotionValues() 10 | 11 | const push = (key: string, value: ResolvedValueTarget, target: MotionProperties, transition: Transition = {}, onComplete?: () => void) => { 12 | // Get the `from` key from target 13 | // @ts-expect-error - Fix errors later for typescript 5 14 | const from = target[key] 15 | 16 | // Get motion value for the target key 17 | const motionValue = get(key, from, target) 18 | 19 | // Sets the value immediately if specified 20 | if (transition && transition.immediate) { 21 | motionValue.set(value) 22 | return 23 | } 24 | 25 | // Create animation 26 | const animation = getAnimation(key, motionValue, value, transition, onComplete) 27 | 28 | // Start animation 29 | motionValue.start(animation) 30 | } 31 | 32 | return { motionValues, stop, push } 33 | } 34 | -------------------------------------------------------------------------------- /tests/useElementTransform.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { nextTick, ref } from 'vue' 3 | import { describe, expect, it } from 'vitest' 4 | import { useElementTransform } from '../src' 5 | 6 | const TestComponent = { 7 | template: '
Hello world
', 8 | } 9 | 10 | function getElementRef() { 11 | const c = mount(TestComponent) 12 | 13 | return ref(c.element as HTMLElement) 14 | } 15 | 16 | describe('useElementTransform', () => { 17 | it('accepts an element', () => { 18 | const element = getElementRef() 19 | 20 | const { transform } = useElementTransform(element) 21 | 22 | expect(transform).toBeDefined() 23 | }) 24 | 25 | it('mutates style properties', () => { 26 | const element = getElementRef() 27 | 28 | const { transform } = useElementTransform(element) 29 | 30 | transform.scale = 1.2 31 | 32 | expect(transform.scale).toBe(1.2) 33 | }) 34 | 35 | it('mutates element properties', async () => { 36 | const element = getElementRef() 37 | 38 | const { transform } = useElementTransform(element) 39 | 40 | transform.translateY = '120px' 41 | 42 | await nextTick() 43 | 44 | expect(element.value.style.transform).toBe('translateY(120px) translateZ(0px)') 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { type Mock, vi } from 'vitest' 2 | import { h } from 'vue' 3 | import { MotionComponent } from '../../src/components' 4 | 5 | export function useCompletionFn() { 6 | return vi.fn(() => {}) 7 | } 8 | 9 | // Get component using either `v-motion` directive or `` component 10 | export function getTestComponent(t: string) { 11 | if (t === 'directive') { 12 | return { template: `
Hello world
` } 13 | } 14 | 15 | return { render: () => h(MotionComponent) } 16 | } 17 | 18 | // Waits until mock has been called and resets the call count 19 | export async function waitForMockCalls(fn: Mock, calls = 1, options: Parameters['1'] = { interval: 10 }) { 20 | try { 21 | await vi.waitUntil(() => fn.mock.calls.length === calls, options) 22 | fn.mockReset() 23 | } 24 | catch (err) { 25 | // This ensures the vitest error log shows where this helper is called instead of the helper internals 26 | if (err instanceof Error) { 27 | err.message += ` Waited for ${calls} call(s) but failed at ${fn.mock.calls.length} call(s).` 28 | 29 | const arr = err.stack?.split('\n') 30 | arr?.splice(0, 3) 31 | err.stack = arr?.join('\n') ?? undefined 32 | } 33 | throw err 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/useMotionFeatures.ts: -------------------------------------------------------------------------------- 1 | import { registerEventListeners } from './features/eventListeners' 2 | import { registerLifeCycleHooks } from './features/lifeCycleHooks' 3 | import { registerVariantsSync } from './features/syncVariants' 4 | import { registerVisibilityHooks } from './features/visibilityHooks' 5 | import type { MotionInstance, MotionVariants, UseMotionOptions } from './types' 6 | 7 | /** 8 | * A Composable executing resolved variants features from variants declarations. 9 | * 10 | * Supports: 11 | * - lifeCycleHooks: Bind the motion hooks to the component lifecycle hooks. 12 | */ 13 | export function useMotionFeatures>( 14 | instance: MotionInstance, 15 | options: UseMotionOptions = { 16 | syncVariants: true, 17 | lifeCycleHooks: true, 18 | visibilityHooks: true, 19 | eventListeners: true, 20 | }, 21 | ) { 22 | // Lifecycle hooks bindings 23 | if (options.lifeCycleHooks) 24 | registerLifeCycleHooks(instance) 25 | 26 | if (options.syncVariants) 27 | registerVariantsSync(instance) 28 | 29 | // Visibility hooks 30 | if (options.visibilityHooks) 31 | registerVisibilityHooks(instance) 32 | 33 | // Event listeners 34 | if (options.eventListeners) 35 | registerEventListeners(instance) 36 | } 37 | -------------------------------------------------------------------------------- /docs/components/content/examples/MotionGroupComponent.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 61 | -------------------------------------------------------------------------------- /playgrounds/vite/src/components/CodeBlock.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 58 | -------------------------------------------------------------------------------- /playgrounds/vite/src/components/DemoBox.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 48 | -------------------------------------------------------------------------------- /playgrounds/vite/src/demos/Transitions.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | -------------------------------------------------------------------------------- /.github/reproduire/needs-reproduction.md: -------------------------------------------------------------------------------- 1 | Would you be able to provide a reproduction? 🙏 2 | 3 |
4 | More info 5 | 6 | ### Why do I need to provide a reproduction? 7 | 8 | Reproductions make it possible for us to triage and fix issues quickly with a relatively small team. It helps us discover the source of the problem, and also can reveal assumptions you or we might be making. 9 | 10 | ### What will happen? 11 | 12 | If you've provided a reproduction, we'll remove the label and try to reproduce the issue. If we can, we'll mark it as a bug and prioritize it based on its severity and how many people we think it might affect. 13 | 14 | If `needs reproduction` labeled issues don't receive any substantial activity (e.g., new comments featuring a reproduction link), we'll close them. That's not because we don't care! At any point, feel free to comment with a reproduction and we'll reopen it. 15 | 16 | ### How can I create a reproduction? 17 | 18 | A link to a stackblitz project or public GitHub repository would be perfect. 👌 19 | 20 | Please ensure that the reproduction is as **minimal** as possible. 21 | 22 | You might also find these other articles interesting and/or helpful: 23 | 24 | - [The Importance of Reproductions](https://antfu.me/posts/why-reproductions-are-required) 25 | - [How to Generate a Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/minimal-reproducible-example) 26 | 27 |
28 | -------------------------------------------------------------------------------- /src/reactiveStyle.ts: -------------------------------------------------------------------------------- 1 | import type { Reactive, Ref } from 'vue' 2 | import { reactive, ref, watch } from 'vue' 3 | import type { StyleProperties } from './types' 4 | import { getValueAsType, getValueType } from './utils/style' 5 | 6 | /** 7 | * Reactive style object implementing all native CSS properties. 8 | * 9 | * @param props 10 | */ 11 | export function reactiveStyle(props: StyleProperties = {}): { state: Reactive, style: Ref } { 12 | // Reactive StyleProperties object 13 | const state = reactive({ 14 | ...props, 15 | }) 16 | 17 | const style = ref({}) 18 | 19 | // Reactive DOM Element compatible `style` object bound to state 20 | watch( 21 | state, 22 | () => { 23 | // Init result object 24 | const result: StyleProperties = {} 25 | 26 | for (const [key, value] of Object.entries(state)) { 27 | // Get value type for key 28 | const valueType = getValueType(key) 29 | // Get value as type for key 30 | const valueAsType = getValueAsType(value, valueType) 31 | // Append the computed style to result object 32 | // @ts-expect-error - Fix errors later for typescript 5 33 | result[key] = valueAsType 34 | } 35 | 36 | style.value = result 37 | }, 38 | { 39 | immediate: true, 40 | deep: true, 41 | }, 42 | ) 43 | 44 | return { 45 | state, 46 | style, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/content/3.api/4.use-motion-properties.md: -------------------------------------------------------------------------------- 1 | # useMotionProperties 2 | 3 | useMotionProperties is used to access [Motion Properties](/features/motion-properties) for a target element. 4 | 5 | Motion properties are combining [useElementStyle](/api/use-element-style) and [useElementTransform](/api/use-element-transform). 6 | 7 | It allows to add another layer between variants and direct element styling, and a cleaner data format from [variants](/features/variants). 8 | 9 | ## Parameters 10 | 11 | ### `target` 12 | 13 | Target must be an element (**SVG** / **HTML**), or a reference to an element. 14 | 15 | If the target reference is updated, the current motion properties will be updated from the new element styling. 16 | 17 | ## Exposed 18 | 19 | ### `motionProperties` 20 | 21 | Motion properties are an object combining [Style Properties](/features/motion-properties#style-properties) and [Transform Properties](/features/motion-properties#transform-properties). 22 | 23 | Change a value and it will be updated on the target element. 24 | 25 | ### `style` 26 | 27 | A style property from [useElementStyle](/api/use-element-style). 28 | 29 | ### `transform` 30 | 31 | A style property from [useElementTransform](/api/use-element-transform). 32 | 33 | ## Example 34 | 35 | ```typescript 36 | const target = ref() 37 | 38 | const { motionProperties } = useMotionProperties(target) 39 | 40 | motionProperties.opacity = 0 41 | 42 | motionProperties.scale = 2 43 | ``` 44 | -------------------------------------------------------------------------------- /playgrounds/vite/src/demos/Editor.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 58 | -------------------------------------------------------------------------------- /src/useMotion.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRef } from 'vue' 2 | import type { MotionInstance, MotionVariants, PermissiveTarget, UseMotionOptions } from './types' 3 | import { useMotionControls } from './useMotionControls' 4 | import { useMotionFeatures } from './useMotionFeatures' 5 | import { useMotionProperties } from './useMotionProperties' 6 | import { useMotionVariants } from './useMotionVariants' 7 | 8 | /** 9 | * A Vue Composable that put your components in motion. 10 | * 11 | * @docs https://motion.vueuse.js.org 12 | * 13 | * @param target 14 | * @param variants 15 | * @param options 16 | */ 17 | export function useMotion>( 18 | target: MaybeRef, 19 | variants: MaybeRef = {} as MaybeRef, 20 | options?: UseMotionOptions, 21 | ) { 22 | // Reactive styling and transform 23 | const { motionProperties } = useMotionProperties(target) 24 | 25 | // Variants manager 26 | const { variant, state } = useMotionVariants(variants) 27 | 28 | // Motion controls, synchronized with motion properties and variants 29 | const controls = useMotionControls(motionProperties, variants) 30 | 31 | // Create motion instance 32 | const instance: MotionInstance = { 33 | target, 34 | variant, 35 | variants, 36 | state, 37 | motionProperties, 38 | ...controls, 39 | } 40 | 41 | // Bind features 42 | useMotionFeatures(instance, options) 43 | 44 | return instance 45 | } 46 | -------------------------------------------------------------------------------- /src/useSpring.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRef } from 'vue' 2 | import { animate } from 'popmotion' 3 | import type { MotionProperties, PermissiveMotionProperties, PermissiveTarget, Spring, SpringControls } from './types' 4 | import { useMotionValues } from './useMotionValues' 5 | import { getDefaultTransition } from './utils/defaults' 6 | 7 | export type UseSpringOptions = Partial & { 8 | target?: MaybeRef 9 | } 10 | 11 | export function useSpring(values: Partial, spring?: UseSpringOptions): SpringControls { 12 | const { stop, get } = useMotionValues() 13 | 14 | return { 15 | values, 16 | stop, 17 | set: (properties: MotionProperties) => 18 | Promise.all( 19 | Object.entries(properties).map(([key, value]) => { 20 | const motionValue = get(key, values[key], values) 21 | 22 | return motionValue.start((onComplete?: () => void) => { 23 | const options = { 24 | type: 'spring', 25 | ...(spring || getDefaultTransition(key, value)), 26 | } as { type: 'spring' | 'decay' | 'keyframes' | undefined } 27 | 28 | return animate({ 29 | from: motionValue.get(), 30 | to: value, 31 | velocity: motionValue.getVelocity(), 32 | onUpdate: v => motionValue.set(v), 33 | onComplete, 34 | ...options, 35 | }) 36 | }) 37 | }), 38 | ), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### 🔗 Linked issue 4 | 5 | 6 | 7 | ### ❓ Type of change 8 | 9 | 10 | 11 | - [ ] 📖 Documentation (updates to the documentation, readme or JSDoc annotations) 12 | - [ ] 🐞 Bug fix (a non-breaking change that fixes an issue) 13 | - [ ] 👌 Enhancement (improving an existing functionality like performance) 14 | - [ ] ✨ New feature (a non-breaking change that adds functionality) 15 | - [ ] 🧹 Chore (updates to the build process or auxiliary tools and libraries) 16 | - [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change) 17 | 18 | ### 📚 Description 19 | 20 | 21 | 22 | 23 | 24 | ### 📝 Checklist 25 | 26 | 27 | 28 | 29 | 30 | - [ ] I have linked an issue or discussion. 31 | - [ ] I have added tests (if possible). 32 | - [ ] I have updated the documentation accordingly. 33 | -------------------------------------------------------------------------------- /playgrounds/vite/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | 51 | 56 | -------------------------------------------------------------------------------- /tests/utils/intersectionObserver.ts: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/thebuilder/react-intersection-observer/blob/d35365990136bfbc99ce112270e5ff232cf45f7f/src/test-helper.ts 2 | // and https://jaketrent.com/post/test-intersection-observer-react/ 3 | import { afterEach, beforeEach, vi } from 'vitest' 4 | 5 | const observerMap = new Map() 6 | const instanceMap = new Map() 7 | 8 | beforeEach(() => { 9 | // @ts-expect-error mocked 10 | window.IntersectionObserver = vi.fn((cb, options = {}) => { 11 | const instance = { 12 | thresholds: Array.isArray(options.threshold) ? options.threshold : [options.threshold], 13 | root: options.root, 14 | rootMargin: options.rootMargin, 15 | observe: vi.fn((element: Element) => { 16 | instanceMap.set(element, instance) 17 | observerMap.set(element, cb) 18 | }), 19 | unobserve: vi.fn((element: Element) => { 20 | instanceMap.delete(element) 21 | observerMap.delete(element) 22 | }), 23 | disconnect: vi.fn(), 24 | } 25 | return instance 26 | }) 27 | }) 28 | 29 | afterEach(() => { 30 | // @ts-expect-error mocked 31 | window.IntersectionObserver.mockReset() 32 | instanceMap.clear() 33 | observerMap.clear() 34 | }) 35 | 36 | export function intersect(element: Element, isIntersecting: boolean) { 37 | const cb = observerMap.get(element) 38 | if (cb) { 39 | cb([ 40 | { 41 | isIntersecting, 42 | target: element, 43 | intersectionRatio: isIntersecting ? 1 : -1, 44 | }, 45 | ]) 46 | } 47 | } 48 | 49 | export function getObserverOf(element: Element): IntersectionObserver { 50 | return instanceMap.get(element) 51 | } 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Report an issue with @vueuse/motion 3 | labels: [pending triage] 4 | body: 5 | - type: textarea 6 | id: bug-env 7 | attributes: 8 | label: System info 9 | description: Output of `npx envinfo --system --npmPackages '{vue,vite,@vueuse/*}' --binaries --browsers` or `npx nuxi info` when using nuxt. 10 | placeholder: System, Binaries, Browsers 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: reproduction 15 | attributes: 16 | label: Reproduction 17 | description: Please provide a link to a repo or [stackblitz](https://stackblitz.com/edit/vitejs-vite-rptfks/) that can reproduce the problem you ran into. A [**minimal reproduction**](https://stackoverflow.com/help/minimal-reproducible-example) is required. 18 | placeholder: Reproduction 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: bug-description 23 | attributes: 24 | label: Describe the bug 25 | description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! 26 | placeholder: Bug description 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: additonal 31 | attributes: 32 | label: Additional context 33 | description: If applicable, add any other context about the problem here 34 | - type: textarea 35 | id: logs 36 | attributes: 37 | label: Logs 38 | description: | 39 | Optional if provided reproduction. Please try not to insert an image but copy paste the log text. 40 | render: shell-script 41 | -------------------------------------------------------------------------------- /src/nuxt/src/module.ts: -------------------------------------------------------------------------------- 1 | import { defu } from 'defu' 2 | import { addComponent, addImportsDir, addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit' 3 | import type { NuxtModule } from '@nuxt/schema' 4 | // @ts-expect-error types exist after build 5 | import type { ModuleOptions as MotionModuleOpts } from '@vueuse/motion' 6 | 7 | export interface ModuleOptions extends MotionModuleOpts {} 8 | 9 | export default defineNuxtModule({ 10 | meta: { 11 | name: '@vueuse/motion', 12 | configKey: 'motion', 13 | }, 14 | defaults: {}, 15 | setup(options, nuxt) { 16 | const { resolve } = createResolver(import.meta.url) 17 | 18 | // Push options and merge to runtimeConfig 19 | nuxt.options.runtimeConfig.public.motion = defu(nuxt.options.runtimeConfig.public.motion || {}, options) 20 | 21 | // Add templates (options and directives) 22 | addPlugin(resolve('./runtime/templates/motion')) 23 | 24 | // Add auto imports 25 | addImportsDir(resolve('./runtime/composables')) 26 | 27 | // Add components 28 | addComponent({ 29 | name: 'Motion', 30 | export: 'MotionComponent', 31 | filePath: '@vueuse/motion', 32 | }) 33 | 34 | addComponent({ 35 | name: 'MotionGroup', 36 | export: 'MotionGroupComponent', 37 | filePath: '@vueuse/motion', 38 | }) 39 | 40 | // Transpile necessary packages 41 | if (!nuxt.options.build.transpile) 42 | nuxt.options.build.transpile = [] 43 | const transpileList = ['defu', '@vueuse/motion', '@vueuse/shared', '@vueuse/core'] 44 | transpileList.forEach((pkgName) => { 45 | if (!nuxt.options.build.transpile.includes(pkgName)) 46 | nuxt.options.build.transpile.push(pkgName) 47 | }) 48 | }, 49 | }) satisfies NuxtModule 50 | -------------------------------------------------------------------------------- /tests/reactiveTransform.spec.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | import { describe, expect, it } from 'vitest' 3 | import { reactiveTransform } from '../src/reactiveTransform' 4 | 5 | describe('reactiveTransform', () => { 6 | it('generate transform from transformProperties', () => { 7 | const { transform } = reactiveTransform( 8 | { 9 | rotateX: 90, 10 | }, 11 | false, 12 | ) 13 | 14 | expect(transform.value).toBe('rotateX(90deg)') 15 | }) 16 | 17 | it('generate a reactive transform string', () => { 18 | const { transform, state } = reactiveTransform( 19 | { 20 | rotateX: 90, 21 | }, 22 | false, 23 | ) 24 | 25 | expect(transform.value).toBe('rotateX(90deg)') 26 | 27 | state.rotateX = 120 28 | 29 | nextTick(() => expect(transform.value).toBe('rotateX(120deg)')) 30 | }) 31 | 32 | it('concatenate the transform string correctly', () => { 33 | const { transform } = reactiveTransform( 34 | { 35 | rotateX: 90, 36 | translateY: 120, 37 | }, 38 | false, 39 | ) 40 | 41 | expect(transform.value).toBe('rotateX(90deg) translateY(120px)') 42 | }) 43 | 44 | it('add the translateZ when hardware acceleration enabled', () => { 45 | const { transform } = reactiveTransform( 46 | { 47 | rotateX: 90, 48 | }, 49 | true, // it is true by default 50 | ) 51 | 52 | expect(transform.value).toBe('rotateX(90deg) translateZ(0px)') 53 | }) 54 | 55 | it('accepts relative units when hardware acceleration is enabled', () => { 56 | const { transform } = reactiveTransform( 57 | { 58 | y: '100%', 59 | }, 60 | true, 61 | ) 62 | 63 | expect(transform.value).toBe('translate3d(0px,100%,0px)') 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/1.introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: "Let's get started with `@vueuse/motion`." 4 | --- 5 | 6 | ::person 7 | :: 8 | 9 | [CodeSandbox](https://codesandbox.io/s/vueusemotion-me1jn?file=/src/components/Person.vue) for this example. 10 | 11 | Illustration from [Pebble People](https://blush.design/fr/collections/pebble-people) by [Deivid Saenz](https://blush.design/fr/artists/deivid-saenz). 12 | 13 | ```bash 14 | yarn add @vueuse/motion 15 | ``` 16 | 17 | ## Plugin Installation 18 | 19 | If you are planning on using the directives (`v-motion`) from this package, you might want to add the plugin to your Vue instance. 20 | 21 | ### Global Installation 22 | 23 | You can add the support for `v-motion` globally, by installing the plugin. 24 | 25 | ```javascript 26 | import { MotionPlugin } from '@vueuse/motion' 27 | 28 | const app = createApp(App) 29 | 30 | app.use(MotionPlugin) 31 | 32 | app.mount('#app') 33 | ``` 34 | 35 | ### Component Installation 36 | 37 | If you want to import the directive code only from components that uses it, import the directive and install it at component level. 38 | 39 | ```javascript 40 | import { MotionDirective as motion } from '@vueuse/motion' 41 | 42 | export default { 43 | directives: { 44 | motion: motion(), 45 | }, 46 | } 47 | ``` 48 | 49 | ## Usage 50 | 51 | - How to use directives? Check out [Directive Usage](/features/directive-usage). 52 | 53 | - What properties you can animate? Check out [Motion Properties](/features/motion-properties). 54 | 55 | - How to create your own animations styles? Check out [Transition Properties](/features/transition-properties). 56 | 57 | - What are variants and how you can use them? Check out [Variants](/features/variants). 58 | 59 | - How to control your declared variants? Check out [Motion Instance](/features/motion-instance). 60 | -------------------------------------------------------------------------------- /src/components/MotionGroup.ts: -------------------------------------------------------------------------------- 1 | import type { Component, PropType, VNode } from 'vue' 2 | 3 | import { Fragment, defineComponent, h, useSlots } from 'vue' 4 | import { variantToStyle } from '../utils/transform' 5 | import { MotionComponentProps, setupMotionComponent } from '../utils/component' 6 | 7 | export default defineComponent({ 8 | name: 'MotionGroup', 9 | props: { 10 | ...MotionComponentProps, 11 | is: { 12 | type: [String, Object] as PropType, 13 | required: false, 14 | }, 15 | }, 16 | setup(props) { 17 | const slots = useSlots() 18 | 19 | const { motionConfig, setNodeInstance } = setupMotionComponent(props) 20 | 21 | return () => { 22 | const style = variantToStyle(motionConfig.value.initial || {}) 23 | const nodes: VNode[] = slots.default?.() || [] 24 | 25 | // Set node style on slots and register to `instances` on mount 26 | for (let i = 0; i < nodes.length; i++) { 27 | const n = nodes[i] 28 | 29 | // Recursively assign fragment child nodes 30 | if (n.type === Fragment && Array.isArray(n.children)) { 31 | n.children.forEach(function setChildInstance(child, index) { 32 | if (child == null) 33 | return 34 | 35 | if (Array.isArray(child)) { 36 | setChildInstance(child, index) 37 | return 38 | } 39 | 40 | if (typeof child === 'object') { 41 | setNodeInstance(child, index, style) 42 | } 43 | }) 44 | } 45 | else { 46 | setNodeInstance(n, i, style) 47 | } 48 | } 49 | 50 | // Wrap child nodes in component if `props.is` is passed 51 | if (props.is) { 52 | return h(props.is, undefined, nodes) 53 | } 54 | 55 | return nodes 56 | } 57 | }, 58 | }) 59 | -------------------------------------------------------------------------------- /docs/components/content/PresetsViewer.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 75 | 76 | 82 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # trigger by `git tag` push only via `yarn release` 4 | on: 5 | push: 6 | branches-ignore: 7 | - '**' 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | strategy: 14 | matrix: 15 | node: [20] 16 | os: [ubuntu-latest] 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Checkout codes 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Enable corepack 27 | run: npm i -fg corepack && corepack enable 28 | 29 | - name: Setup node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node }} 33 | cache: pnpm 34 | 35 | - name: Install dependencies 36 | run: pnpm install --frozen-lockfile 37 | 38 | - name: Create github releases 39 | run: | 40 | npx changelogithub 41 | env: 42 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 43 | 44 | - name: Extract version tag 45 | if: startsWith( github.ref, 'refs/tags/v' ) 46 | uses: jungwinter/split@v2 47 | id: split 48 | with: 49 | msg: ${{ github.ref }} 50 | separator: / 51 | 52 | - name: Sync changelog from github releases 53 | run: | 54 | pnpm changelog --tag=${{ steps.split.outputs._2 }} 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | 58 | - name: Commit changelog 59 | uses: stefanzweifel/git-auto-commit-action@v5 60 | with: 61 | branch: main 62 | file_pattern: CHANGELOG.md 63 | commit_message: 'chore: generate changelog' 64 | 65 | - name: Publish package 66 | run: ./scripts/release.sh 67 | env: 68 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 69 | -------------------------------------------------------------------------------- /playgrounds/nuxt/content/index.md: -------------------------------------------------------------------------------- 1 | # Hello @vueuse/motion 2 | 3 | This is a preview of `` component used with @nuxt/content. 4 | 5 | In the middle of a :motion{is="span" style="display: inline-block;" preset="slideBottom"}[phrase] :wave: :rocket: 6 | 7 | ::div{style="display: flex; align-items: center; flex-direction: column;"} 8 | 9 | ::div{style="height: 4rem;"} 10 | :: 11 | 12 | ::motion{preset="slideVisibleRight"} 13 | ::block{color="#453A49"} 14 | Hello :wave: 15 | :: 16 | :: 17 | 18 | ::div{style="height: 4rem;"} 19 | :: 20 | 21 | ::motion{preset="slideVisibleBottom"} 22 | ::block{color="#6D3B47"} 23 | Hello world 24 | :: 25 | :: 26 | 27 | ::div{style="height: 4rem;"} 28 | :: 29 | 30 | ::motion{preset="slideVisibleLeft"} 31 | ::block{color="#BA2C73"} 32 | Hello world 33 | :: 34 | :: 35 | 36 | ::div{style="height: 4rem;"} 37 | :: 38 | 39 | ::motion{preset="slideVisibleBottom"} 40 | ::block{color="#453A49"} 41 | Hello world 42 | :: 43 | :: 44 | 45 | ::div{style="height: 4rem;"} 46 | :: 47 | 48 | ::motion{preset="slideVisibleLeft"} 49 | ::block{color="#6D3B47"} 50 | Hello world 51 | :: 52 | :: 53 | 54 | ::div{style="height: 4rem;"} 55 | :: 56 | 57 | ::motion{preset="slideVisibleBottom"} 58 | ::block{color="#BA2C73"} 59 | Hello world 60 | :: 61 | :: 62 | 63 | ::div{style="height: 4rem;"} 64 | :: 65 | 66 | ::motion{preset="slideVisibleLeft"} 67 | ::block{color="#453A49"} 68 | Hello world 69 | :: 70 | :: 71 | 72 | ::div{style="height: 4rem;"} 73 | :: 74 | 75 | ::motion{preset="slideVisibleBottom"} 76 | ::block{color="#6D3B47"} 77 | Hello world 78 | :: 79 | :: 80 | 81 | ::div{style="height: 4rem;"} 82 | :: 83 | 84 | ::motion{preset="slideVisibleLeft"} 85 | ::block{color="#BA2C73"} 86 | Hello world 87 | :: 88 | :: 89 | 90 | :: 91 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./docs/.nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "jsx": "preserve", 6 | "moduleDetection": "force", 7 | "baseUrl": ".", 8 | "module": "preserve", 9 | "moduleResolution": "bundler", 10 | "paths": { 11 | "@vueuse/motion": ["./src/index"], 12 | "@vueuse/motion/nuxt": ["./src/nuxt/module"], 13 | "#app": ["./node_modules/nuxt/dist/app"], 14 | "#app/*": ["./node_modules/nuxt/dist/app/*"], 15 | "#color-mode-options": ["./docs/.nuxt/color-mode-options"], 16 | "#nuxt-component-meta": ["./docs/.nuxt/component-meta.mjs"], 17 | "#nuxt-component-meta/types": ["./docs/.nuxt/component-meta.d.ts"], 18 | "#vue-router": ["./docs/.nuxt/vue-router"], 19 | "#imports": ["./docs/.nuxt/imports"], 20 | "#build": ["./docs/.nuxt"], 21 | "#build/*": ["./docs/.nuxt/*"], 22 | "@nuxt-themes/tokens/config": [ 23 | "./node_modules/@nuxt-themes/tokens/dist/tokens.config.ts" 24 | ], 25 | "#pinceau/utils": ["./docs/.nuxt/pinceau/utils.ts"], 26 | "#pinceau/theme": ["./docs/.nuxt/pinceau/index.ts"], 27 | "#pinceau/schema": ["./docs/.nuxt/pinceau/schema.ts"], 28 | "#pinceau/definitions": ["./docs/.nuxt/pinceau/definitions.ts"], 29 | "#components": [".nuxt/components"] 30 | }, 31 | "resolveJsonModule": true, 32 | "types": ["node", "vitest/globals", "vite/client"], 33 | "allowJs": true, 34 | "strict": true, 35 | "noEmit": true, 36 | "allowSyntheticDefaultImports": true, 37 | "forceConsistentCasingInFileNames": true, 38 | "skipLibCheck": true 39 | }, 40 | "include": [ 41 | "./env.d.ts", 42 | "docs/**/*", 43 | "src/**/*", 44 | "tests/**/*", 45 | "docs/**/*", 46 | "playgrounds/**/*" 47 | ], 48 | "exclude": ["../dist", "../.output"], 49 | "vueCompilerOptions": { 50 | "plugins": ["pinceau/volar"] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/content/2.features/3.motion-properties.md: -------------------------------------------------------------------------------- 1 | # Motion Properties 2 | 3 | Motion properties are represented by an object containing all the animatable properties of an element. 4 | 5 | They are one of the two parts that compose a [Variant](/features/variants), with [Transitions Declaration](/features/transition-properties). 6 | 7 | This object contains both style and transform properties. 8 | 9 | Note that when interacting with both style and transform properties, you are not forced to specify units and can instead just use numbers. 10 | 11 | The default unit of the vast majority of attributes will be resolved and appended to the value dynamically. 12 | 13 | ```javascript 14 | { 15 | opacity: 0, 16 | scale: 0.6, 17 | y: 100 18 | } 19 | ``` 20 | 21 | ##### _An example of motion properties_ ☝️ 22 | 23 | ## Style Properties 24 | 25 | Style properties are used to decompose a regular `style` CSS string into individual object keys. 26 | 27 | The typings are the same as the regular `style` property from Vue templates. 28 | 29 | All the regular [CSS Style](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference) properties are supported. 30 | 31 | Some keys are **forbidden** (`transition`, `rotate`, `scale`...) as they are now reserved for [Transform Properties](#transform-properties). 32 | 33 | ```javascript 34 | { 35 | opacity: 0, 36 | marginTop: 10 37 | } 38 | ``` 39 | 40 | ##### _An example of style properties_ ☝️ 41 | 42 | ## Transform Properties 43 | 44 | Transform properties are used to decompose a regular `transform` CSS string into individual object keys. 45 | 46 | All the regular [CSS Transform](https://developer.mozilla.org/en-US/docs/Web/CSS/transform#syntax) syntax arguments are supported. 47 | 48 | As an addition, you can use `x`, `y`, `z` properties, that will be converted into `translateX`, `translateY` and `translateZ`. 49 | 50 | ```javascript 51 | { 52 | x: 10, 53 | y: 20, 54 | scale: 1.2 55 | } 56 | ``` 57 | 58 | ##### _An example of transform properties_ ☝️ 59 | -------------------------------------------------------------------------------- /src/useMotionValues.ts: -------------------------------------------------------------------------------- 1 | import { tryOnUnmounted } from '@vueuse/shared' 2 | import { ref } from 'vue' 3 | import type { Ref } from 'vue' 4 | import type { MotionValue } from './motionValue' 5 | import { getMotionValue } from './motionValue' 6 | import type { MotionProperties, MotionValuesMap } from './types' 7 | 8 | const { isArray } = Array 9 | 10 | export function useMotionValues() { 11 | const motionValues = ref({}) as Ref 12 | 13 | const stop = (keys?: string | string[]) => { 14 | // Destroy key closure 15 | const destroyKey = (key: string) => { 16 | if (!motionValues.value[key]) 17 | return 18 | 19 | motionValues.value[key].stop() 20 | motionValues.value[key].destroy() 21 | 22 | delete motionValues.value[key] 23 | } 24 | 25 | // Check if keys argument is defined 26 | if (keys) { 27 | if (isArray(keys)) { 28 | // If `keys` are an array, loop on specified keys and destroy them 29 | keys.forEach(destroyKey) 30 | } 31 | else { 32 | // If `keys` is a string, destroy the specified one 33 | destroyKey(keys) 34 | } 35 | } 36 | else { 37 | // No keys specified, destroy all animations 38 | Object.keys(motionValues.value).forEach(destroyKey) 39 | } 40 | } 41 | 42 | const get = (key: string, from: any, target: MotionProperties): MotionValue => { 43 | if (motionValues.value[key]) 44 | return motionValues.value[key] as MotionValue 45 | 46 | // Create motion value 47 | const motionValue = getMotionValue(from) 48 | 49 | // Set motion properties mapping 50 | // @ts-expect-error - Fix errors later for typescript 5 51 | motionValue.onChange(v => (target[key] = v)) 52 | 53 | // Set instance motion value 54 | motionValues.value[key] = motionValue 55 | 56 | return motionValue 57 | } 58 | 59 | // Ensure everything is cleared on unmount 60 | tryOnUnmounted(stop) 61 | 62 | return { 63 | motionValues, 64 | get, 65 | stop, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/useElementTransform.ts: -------------------------------------------------------------------------------- 1 | import { watch } from 'vue' 2 | import type { MaybeRef, Reactive } from 'vue' 3 | import { reactiveTransform } from './reactiveTransform' 4 | import type { MotionTarget, PermissiveTarget, TransformProperties } from './types' 5 | import { usePermissiveTarget } from './usePermissiveTarget' 6 | import { stateFromTransform } from './utils/transform-parser' 7 | 8 | /** 9 | * A Composable giving access to a TransformProperties object, and binding the generated transform string to a target. 10 | * 11 | * @param target 12 | */ 13 | export function useElementTransform(target: MaybeRef, onInit?: (initData: Partial) => void): { transform: Reactive } { 14 | // Transform cache available before the element is mounted 15 | let _cache: string | undefined 16 | // Local target cache as we need to resolve the element from PermissiveTarget 17 | let _target: MotionTarget 18 | // Create a reactive transform object 19 | const { state, transform } = reactiveTransform() 20 | 21 | // Cache transform until the element is alive and we can bind to it 22 | usePermissiveTarget(target, (el) => { 23 | _target = el 24 | 25 | // Parse transform properties and applies them to the current state 26 | if (el.style.transform) 27 | stateFromTransform(state, el.style.transform) 28 | 29 | // If cache is present, init the target with the current cached value 30 | if (_cache) 31 | el.style.transform = _cache 32 | 33 | if (onInit) 34 | onInit(state) 35 | }) 36 | 37 | // Sync reactive transform to element 38 | watch( 39 | transform, 40 | (newValue) => { 41 | // Add the current value to the cache so it is set on target creation 42 | if (!_target) { 43 | _cache = newValue 44 | return 45 | } 46 | 47 | // Set the transform string on the target 48 | _target.style.transform = newValue 49 | }, 50 | { 51 | immediate: true, 52 | }, 53 | ) 54 | 55 | return { 56 | transform: state, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { MotionPluginOptions, MotionVariants } from '../types' 3 | 4 | import * as presets from '../presets' 5 | import { directive } from '../directive' 6 | import { slugify } from '../utils/slugify' 7 | import { MotionComponent, MotionGroupComponent } from '../components' 8 | import { CUSTOM_PRESETS } from '../utils/keys' 9 | 10 | export const MotionPlugin = { 11 | install(app: App, options?: MotionPluginOptions) { 12 | // Register default `v-motion` directive 13 | app.directive('motion', directive()) 14 | 15 | // Register presets 16 | if (!options || (options && !options.excludePresets)) { 17 | for (const key in presets) { 18 | // Get preset variants 19 | // @ts-expect-error - Fix errors later for typescript 5 20 | const preset = presets[key] 21 | 22 | // Register the preset `v-motion-${key}` directive 23 | app.directive(`motion-${slugify(key)}`, directive(preset, true)) 24 | } 25 | } 26 | 27 | // Register plugin-wise directives 28 | if (options && options.directives) { 29 | // Loop on options, create a custom directive for each definition 30 | for (const key in options.directives) { 31 | // Get directive variants 32 | const variants = options.directives[key] as MotionVariants 33 | 34 | // Development warning, showing definitions missing `initial` key 35 | if (!variants.initial && import.meta.env?.MODE === 'development') { 36 | console.warn( 37 | `Your directive v-motion-${key} is missing initial variant!`, 38 | ) 39 | } 40 | 41 | // Register the custom `v-motion-${key}` directive 42 | app.directive(`motion-${key}`, directive(variants, true)) 43 | } 44 | } 45 | 46 | app.provide(CUSTOM_PRESETS, options?.directives) 47 | 48 | // Register component 49 | app.component('Motion', MotionComponent) 50 | 51 | // Register component 52 | app.component('MotionGroup', MotionGroupComponent) 53 | }, 54 | } 55 | 56 | export default MotionPlugin 57 | -------------------------------------------------------------------------------- /src/useMotionProperties.ts: -------------------------------------------------------------------------------- 1 | import { reactive, watch } from 'vue' 2 | import type { MaybeRef, Reactive } from 'vue' 3 | import type { MotionProperties, PermissiveTarget, StyleProperties, TransformProperties } from './types' 4 | import { useElementStyle } from './useElementStyle' 5 | import { useElementTransform } from './useElementTransform' 6 | import { usePermissiveTarget } from './usePermissiveTarget' 7 | import { isTransformProp } from './utils/transform' 8 | import { objectEntries } from './utils/type-feature' 9 | 10 | /** 11 | * A Composable giving access to both `transform` and `style`objects for a single element. 12 | * 13 | * @param target 14 | */ 15 | export function useMotionProperties(target: MaybeRef, defaultValues?: Partial): { motionProperties: Reactive, style: Reactive, transform: Reactive } { 16 | // Local motion properties 17 | const motionProperties = reactive({}) 18 | 19 | // Local mass setter 20 | // @ts-expect-error - Fix errors later for typescript 5 21 | const apply = (values: Partial) => Object.entries(values).forEach(([key, value]) => (motionProperties[key] = value)) 22 | 23 | // Target element style object 24 | const { style } = useElementStyle(target, apply) 25 | 26 | // Target element transform object 27 | const { transform } = useElementTransform(target, apply) 28 | 29 | // Watch local object and apply styling accordingly 30 | watch( 31 | motionProperties, 32 | (newVal) => { 33 | objectEntries(newVal).forEach(([key, value]) => { 34 | const target = isTransformProp(key) ? transform : style 35 | if (target[key] && target[key] === value) 36 | return 37 | target[key] = value 38 | }) 39 | }, 40 | { 41 | immediate: true, 42 | deep: true, 43 | }, 44 | ) 45 | 46 | // Apply default values once target is available 47 | usePermissiveTarget(target, () => defaultValues && apply(defaultValues)) 48 | 49 | return { 50 | motionProperties, 51 | style, 52 | transform, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /playgrounds/vite/src/demos/Delay.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 81 | -------------------------------------------------------------------------------- /docs/content/3.api/7.use-motion-controls.md: -------------------------------------------------------------------------------- 1 | # useMotionControls 2 | 3 | useMotionControls is used to create motion controls from motion properties and motion transitions. 4 | 5 | [Motion Instance](/features/motion-instance) members are **helpers** for you to interact with your element motion properties with ease. 6 | 7 | ## Parameters 8 | 9 | ### `motionProperties` 10 | 11 | A [Motion Properties](/api/use-motion-properties) instance. 12 | 13 | ### `variants` 14 | 15 | A [Variants](/features/variants#custom-variants) definition. 16 | 17 | ### `motionTransitions` 18 | 19 | A [Motion Transitions](/api/use-motion-transitions) instance. 20 | 21 | ## Exposed 22 | 23 | ### `apply(variant)` 24 | 25 | Apply function will take a [Variant Definition](/features/variants) and apply it to the element without changing the current variant value. 26 | 27 | It also accepts a variant key from variants parameter, that will be applied without changing the current variant name. 28 | 29 | Apply is a promise that will be resolved once all the transitions resulting from the variant you passed are done. 30 | 31 | ### `set(variant)` 32 | 33 | Set function will take a [Variant Definition](/features/variants) and apply it to the element without changing the current variant value. 34 | 35 | It also accepts a variant key from variants parameter, that will be applied without changing the current variant name. 36 | 37 | It differs from `apply(variant)` as it will set values on target without running any transitions. 38 | 39 | ### `stopTransitions()` 40 | 41 | Stop Transitions function will stop all ongoing transitions on the current [useMotionTransitions](/api/use-motion-transitions) instance. 42 | 43 | ## Example 44 | 45 | ```typescript 46 | const target = ref() 47 | 48 | const { motionProperties } = useMotionProperties(target) 49 | 50 | const { apply, stopTransitions } = useMotionControls(motionProperties, { 51 | initial: { 52 | y: 100, 53 | opacity: 0, 54 | }, 55 | custom: { 56 | y: 0, 57 | opacity: 1, 58 | }, 59 | }) 60 | 61 | apply({ 62 | opacity: 1, 63 | scale: 2, 64 | }) 65 | 66 | setTimeout(() => { 67 | stopTransitions() 68 | 69 | console.log('Stopped after 200ms!') 70 | }, 200) 71 | 72 | const applyCustom = async () => { 73 | await apply('custom') 74 | 75 | console.log('Custom applied!') 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /src/utils/defaults.ts: -------------------------------------------------------------------------------- 1 | import type { Keyframes, KeyframesTarget, PopmotionTransitionProps, SingleTarget, Spring, ValueTarget } from '../types' 2 | 3 | export function isKeyframesTarget(v: ValueTarget): v is KeyframesTarget { 4 | return Array.isArray(v) 5 | } 6 | 7 | export function underDampedSpring(): Partial { 8 | return { 9 | type: 'spring', 10 | stiffness: 500, 11 | damping: 25, 12 | restDelta: 0.5, 13 | restSpeed: 10, 14 | } 15 | } 16 | 17 | export function criticallyDampedSpring(to: SingleTarget) { 18 | return { 19 | type: 'spring', 20 | stiffness: 550, 21 | damping: to === 0 ? 2 * Math.sqrt(550) : 30, 22 | restDelta: 0.01, 23 | restSpeed: 10, 24 | } 25 | } 26 | 27 | export function overDampedSpring(to: SingleTarget): Partial { 28 | return { 29 | type: 'spring', 30 | stiffness: 550, 31 | damping: to === 0 ? 100 : 30, 32 | restDelta: 0.01, 33 | restSpeed: 10, 34 | } 35 | } 36 | 37 | export function linearTween(): Partial { 38 | return { 39 | type: 'keyframes', 40 | ease: 'linear', 41 | duration: 300, 42 | } 43 | } 44 | 45 | function keyframes(values: KeyframesTarget): Partial { 46 | return { 47 | type: 'keyframes', 48 | duration: 800, 49 | values, 50 | } 51 | } 52 | 53 | type TransitionFactory = (to: ValueTarget) => Partial 54 | 55 | const defaultTransitions = { 56 | default: overDampedSpring, 57 | x: underDampedSpring, 58 | y: underDampedSpring, 59 | z: underDampedSpring, 60 | rotate: underDampedSpring, 61 | rotateX: underDampedSpring, 62 | rotateY: underDampedSpring, 63 | rotateZ: underDampedSpring, 64 | scaleX: criticallyDampedSpring, 65 | scaleY: criticallyDampedSpring, 66 | scale: criticallyDampedSpring, 67 | backgroundColor: linearTween, 68 | color: linearTween, 69 | opacity: linearTween, 70 | } 71 | 72 | export function getDefaultTransition(valueKey: string, to: ValueTarget): PopmotionTransitionProps { 73 | let transitionFactory: TransitionFactory 74 | 75 | if (isKeyframesTarget(to)) { 76 | transitionFactory = keyframes as TransitionFactory 77 | } 78 | else { 79 | // @ts-expect-error - Fix errors later for typescript 5 80 | transitionFactory = defaultTransitions[valueKey] || defaultTransitions.default 81 | } 82 | 83 | return { to, ...transitionFactory(to) } as PopmotionTransitionProps 84 | } 85 | -------------------------------------------------------------------------------- /src/utils/directive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '@vueuse/core' 2 | import type { Ref, VNode } from 'vue' 3 | import type { MotionVariants } from '../types' 4 | 5 | const transitionKeys = ['delay', 'duration'] as const 6 | const directivePropsKeys = ['initial', 'enter', 'leave', 'visible', 'visible-once', 'visibleOnce', 'hovered', 'tapped', 'focused', ...transitionKeys] as const 7 | 8 | function isTransitionKey(val: any): val is 'delay' | 'duration' { 9 | return transitionKeys.includes(val) 10 | } 11 | 12 | export function resolveVariants(node: VNode>, variantsRef: Ref>) { 13 | // This is done to achieve compat with Vue 2 & 3 14 | // node.props = Vue 3 element props location 15 | // node.data.attrs = Vue 2 element props location 16 | const target = node.props 17 | ? node.props // @ts-expect-error - Compatibility (Vue 3) 18 | : node.data && node.data.attrs // @ts-expect-error - Compatibility (Vue 2) 19 | ? node.data.attrs 20 | : {} 21 | 22 | if (target) { 23 | if (target.variants && isObject(target.variants)) { 24 | // If variant are passed through a single object reference, initialize with it 25 | variantsRef.value = { 26 | ...variantsRef.value, 27 | ...target.variants, 28 | } 29 | } 30 | 31 | // Loop on directive prop keys, add them to the local variantsRef if defined 32 | for (let key of directivePropsKeys) { 33 | if (!target || !target[key]) 34 | continue 35 | 36 | if (isTransitionKey(key) && typeof target[key] === 'number') { 37 | // Apply transition property to existing variants where applicable 38 | for (const variantKey of ['enter', 'visible', 'visibleOnce'] as const) { 39 | const variantConfig = variantsRef.value[variantKey] 40 | 41 | if (variantConfig == null) 42 | continue 43 | 44 | variantConfig.transition ??= {} 45 | // @ts-expect-error `duration` does not exist on `inertia` type transitions 46 | variantConfig.transition[key] = target[key] 47 | } 48 | 49 | continue 50 | } 51 | 52 | if (isObject(target[key])) { 53 | const prop = target[key] 54 | if (key === 'visible-once') 55 | key = 'visibleOnce' 56 | variantsRef.value[key as keyof MotionVariants] = prop 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/2.nuxt.md: -------------------------------------------------------------------------------- 1 | # Nuxt Usage 2 | 3 | If you are using [Nuxt](https://nuxtjs.org/), this package has a specific implementation that makes the declaration of custom directives even easier. 4 | 5 | It is shipped with `@vueuse/motion` and is importable via `@vueuse/motion/nuxt`. 6 | 7 | It should work with `nuxt3` and `@nuxt/bridge`. 8 | 9 | ## Installation 10 | 11 | Add `@vueuse/motion/nuxt` to the `modules` section of `nuxt.config.js`: 12 | 13 | ```javascript 14 | { 15 | // nuxt.config.js 16 | modules: ['@vueuse/motion/nuxt'] 17 | } 18 | ``` 19 | 20 | Then, configure your animations 🤹: 21 | 22 | ```javascript 23 | { 24 | // nuxt.config.js 25 | runtimeConfig: { 26 | public: { 27 | motion: { 28 | directives: { 29 | 'pop-bottom': { 30 | initial: { 31 | scale: 0, 32 | opacity: 0, 33 | y: 100, 34 | }, 35 | visible: { 36 | scale: 1, 37 | opacity: 1, 38 | y: 0, 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | ## SSR Support 49 | 50 | `@vueuse/motion` supports SSR via both directives and `` component. 51 | 52 | SSR support for animations mainly consists in resolving `initial` variant from your component bindings. 53 | 54 | Once resolve, this `initial` value gets merged with your component `style` attribute. 55 | 56 | ```vue 57 | 83 | 84 | 95 | ``` 96 | 97 | This div will have be rendered server-side as: 98 | 99 | ```html 100 |
Hello
101 | ``` 102 | 103 | You can obviously imagine plenty of implementations with this, always knowing that your animations will be properly server-side rendered. 104 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] # macos-latest, windows-latest 16 | node: [20] 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | fetch-depth: 0 26 | 27 | - name: Enable corepack 28 | run: npm i -fg corepack && corepack enable 29 | 30 | - name: Use Node.js ${{ matrix.node }} 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node }} 34 | registry-url: https://registry.npmjs.org/ 35 | cache: pnpm 36 | 37 | - name: Install dependencies 38 | run: pnpm install 39 | 40 | - name: Lint project 41 | run: pnpm lint 42 | 43 | - name: Test project 44 | run: pnpm test 45 | 46 | - name: Build project 47 | run: pnpm build 48 | 49 | - name: Cache dist 50 | uses: actions/cache@v4 51 | with: 52 | path: dist 53 | key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} 54 | 55 | nightly-release: 56 | needs: 57 | - test 58 | strategy: 59 | matrix: 60 | node: [20] 61 | os: [ubuntu-latest] 62 | 63 | runs-on: ${{ matrix.os }} 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | - run: npm i -fg corepack && corepack enable 68 | 69 | - uses: actions/setup-node@v4 70 | with: 71 | node-version: ${{ matrix.node }} 72 | cache: pnpm 73 | 74 | - name: Install dependencies 75 | run: pnpm install --frozen-lockfile 76 | 77 | - name: Restore dist cache 78 | uses: actions/cache@v4 79 | with: 80 | path: dist 81 | key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} 82 | 83 | - name: Release Nightly 84 | if: | 85 | github.event_name == 'push' && 86 | !startsWith(github.event.head_commit.message, '[skip-release]') && 87 | !startsWith(github.event.head_commit.message, 'chore') && 88 | !startsWith(github.event.head_commit.message, 'docs') 89 | run: ./scripts/release-nightly.sh 90 | env: 91 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 92 | -------------------------------------------------------------------------------- /src/reactiveTransform.ts: -------------------------------------------------------------------------------- 1 | import { px } from 'style-value-types' 2 | import { reactive, ref, watch } from 'vue' 3 | import type { Reactive, Ref } from 'vue' 4 | import type { TransformProperties } from './types' 5 | import { getValueAsType, getValueType } from './utils/style' 6 | 7 | /** 8 | * Aliases translate key for simpler API integration. 9 | */ 10 | const translateAlias: Record = { 11 | x: 'translateX', 12 | y: 'translateY', 13 | z: 'translateZ', 14 | } 15 | 16 | /** 17 | * Reactive transform string implementing all native CSS transform properties. 18 | * 19 | * @param props 20 | * @param enableHardwareAcceleration 21 | */ 22 | export function reactiveTransform(props: TransformProperties = {}, enableHardwareAcceleration = true): { state: Reactive, transform: Ref } { 23 | // Reactive TransformProperties object 24 | const state = reactive({ ...props }) 25 | 26 | const transform = ref('') 27 | 28 | watch( 29 | state, 30 | (newVal) => { 31 | // Init result 32 | let result = '' 33 | let hasHardwareAcceleration = false 34 | 35 | // Use translate3d by default has a better GPU optimization 36 | // And corrects scaling discrete behaviors 37 | if (enableHardwareAcceleration && (newVal.x || newVal.y || newVal.z)) { 38 | const str = [newVal.x || 0, newVal.y || 0, newVal.z || 0] 39 | .map(val => getValueAsType(val, px)) 40 | .join(',') 41 | 42 | result += `translate3d(${str}) ` 43 | 44 | hasHardwareAcceleration = true 45 | } 46 | 47 | // Loop on defined TransformProperties state keys 48 | for (const [key, value] of Object.entries(newVal)) { 49 | if (enableHardwareAcceleration && (key === 'x' || key === 'y' || key === 'z')) 50 | continue 51 | 52 | // Get value type for key 53 | const valueType = getValueType(key) 54 | // Get value as type for key 55 | const valueAsType = getValueAsType(value, valueType) 56 | // Append the computed transform key to result string 57 | result += `${translateAlias[key] || key}(${valueAsType}) ` 58 | } 59 | 60 | if (enableHardwareAcceleration && !hasHardwareAcceleration) 61 | result += 'translateZ(0px) ' 62 | 63 | transform.value = result.trim() 64 | }, 65 | { 66 | immediate: true, 67 | deep: true, 68 | }, 69 | ) 70 | 71 | return { 72 | state, 73 | transform, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/useElementStyle.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRef, Reactive } from 'vue' 2 | import { watch } from 'vue' 3 | import { reactiveStyle } from './reactiveStyle' 4 | import type { MotionTarget, PermissiveTarget, StyleProperties } from './types' 5 | import { usePermissiveTarget } from './usePermissiveTarget' 6 | import { valueTypes } from './utils/style' 7 | import { isTransformOriginProp, isTransformProp } from './utils/transform' 8 | 9 | /** 10 | * A Composable giving access to a StyleProperties object, and binding the generated style object to a target. 11 | * 12 | * @param target 13 | */ 14 | export function useElementStyle(target: MaybeRef, onInit?: (initData: Partial) => void): { style: Reactive } { 15 | // Transform cache available before the element is mounted 16 | let _cache: StyleProperties | undefined 17 | // Local target cache as we need to resolve the element from PermissiveTarget 18 | let _target: MotionTarget 19 | // Create a reactive style object 20 | const { state, style } = reactiveStyle() 21 | 22 | usePermissiveTarget(target, (el) => { 23 | _target = el 24 | 25 | // Loop on style keys 26 | for (const key of Object.keys(valueTypes)) { 27 | // @ts-expect-error - Fix errors later for typescript 5 28 | if (el.style[key] === null || el.style[key] === '' || isTransformProp(key) || isTransformOriginProp(key)) 29 | continue 30 | 31 | // Append a defined key to the local StyleProperties state object 32 | // @ts-expect-error - Fix errors later for typescript 5 33 | state[key] = el.style[key] 34 | } 35 | 36 | // If cache is present, init the target with the current cached value 37 | if (_cache) { 38 | // @ts-expect-error - Fix errors later for typescript 5 39 | Object.entries(_cache).forEach(([key, value]) => (el.style[key] = value)) 40 | } 41 | 42 | if (onInit) 43 | onInit(state) 44 | }) 45 | 46 | // Sync reactive style to element 47 | watch( 48 | style, 49 | (newVal) => { 50 | // Add the current value to the cache so it is set on target creation 51 | if (!_target) { 52 | _cache = newVal 53 | return 54 | } 55 | 56 | // Append the state object to the target style properties 57 | // @ts-expect-error - Fix errors later for typescript 5 58 | for (const key in newVal) _target.style[key] = newVal[key] 59 | }, 60 | { 61 | immediate: true, 62 | }, 63 | ) 64 | 65 | return { 66 | style: state, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/content/3.api/1.use-motion.md: -------------------------------------------------------------------------------- 1 | # useMotion 2 | 3 | useMotion is the core composable of this package. 4 | 5 | This composable imports every other composable and expose a motion instance. 6 | 7 | [useMotionProperties](/api/use-motion-properties) is used to make the element styling properties reactive. 8 | 9 | [useMotionTransitions](/api/use-motion-transitions) is used to manage transitions scheduling and execution. 10 | 11 | [useMotionVariants](/api/use-motion-variants) is used to handle variants and variant selection. 12 | 13 | [useMotionControls](/api/use-motion-controls) is used to create motion controls from variants, properties and transitions. 14 | 15 | [useMotionFeatures](/api/use-motion-features) is used to register lifecycle hooks bindings, visibility and events listeners. 16 | 17 | ## Parameters 18 | 19 | ### `target` 20 | 21 | Target must be an element (**SVG** / **HTML**), or a reference to an element. 22 | 23 | If the target reference is updated, the current variant will be applied to the new element. 24 | 25 | ### `variants` 26 | 27 | **Variants** must be an object or an object reference. 28 | 29 | Keys are variants names, values are [Variants Declarations](/features/variants). 30 | 31 | ### `options` 32 | 33 | Options is an object, supporting **4** parameters: 34 | 35 | 1. `syncVariants` (boolean): Whether or not the variants will be synchronized on update. 36 | 2. `lifeCycleHooks` (boolean): Whether or not the lifecycle hooks will be followed. 37 | 3. `visibilityHooks` (boolean): Whether or not the visibility hooks will be applied. 38 | 4. `eventListeners` (boolean): Whether or not the event listeners will be registered. 39 | 40 | You should not be pushed to use those options, as if you are not declaring the related variants, they will not be registered anyway. 41 | 42 | ## Exposed 43 | 44 | ### `target` 45 | 46 | Target is a reference to the element you passed as a parameter. 47 | 48 | ### `variant` 49 | 50 | Variant is a string reference, from [useMotionVariants](/api/use-motion-variants). 51 | 52 | ### `variants` 53 | 54 | Variants is a reference to the variants you passed as a parameter. 55 | 56 | ### `state` 57 | 58 | State is a computed reference to the current variant applied to your element. 59 | 60 | ### `...controls` 61 | 62 | Spread object from [Motion Controls](/api/use-motion-controls) exposed functions. 63 | 64 | ## Example 65 | 66 | ```typescript 67 | const target = ref() 68 | 69 | const variants = ref({ 70 | initial: { 71 | opacity: 0, 72 | }, 73 | enter: { 74 | opacity: 1, 75 | }, 76 | }) 77 | 78 | const motionInstance = useMotion(target, variants) 79 | ``` 80 | -------------------------------------------------------------------------------- /src/utils/transform.ts: -------------------------------------------------------------------------------- 1 | import { reactiveStyle } from '../reactiveStyle' 2 | import { reactiveTransform } from '../reactiveTransform' 3 | import type { Variant } from './../types/variants' 4 | 5 | /** 6 | * A list of all transformable axes. We'll use this list to generated a version 7 | * of each axes for each transform. 8 | */ 9 | export const transformAxes = ['', 'X', 'Y', 'Z'] 10 | 11 | /** 12 | * An ordered array of each transformable value. By default, transform values 13 | * will be sorted to this order. 14 | */ 15 | const order = ['perspective', 'translate', 'scale', 'rotate', 'skew'] 16 | 17 | /** 18 | * Generate a list of every possible transform key. 19 | */ 20 | export const transformProps = ['transformPerspective', 'x', 'y', 'z'] 21 | order.forEach((operationKey) => { 22 | transformAxes.forEach((axesKey) => { 23 | const key = operationKey + axesKey 24 | transformProps.push(key) 25 | }) 26 | }) 27 | 28 | /** 29 | * A function to use with Array.sort to sort transform keys by their default order. 30 | */ 31 | export function sortTransformProps(a: string, b: string) { 32 | return transformProps.indexOf(a) - transformProps.indexOf(b) 33 | } 34 | 35 | /** 36 | * A quick lookup for transform props. 37 | */ 38 | const transformPropSet = new Set(transformProps) 39 | export function isTransformProp(key: string) { 40 | return transformPropSet.has(key) 41 | } 42 | 43 | /** 44 | * A quick lookup for transform origin props 45 | */ 46 | const transformOriginProps = new Set(['originX', 'originY', 'originZ']) 47 | export function isTransformOriginProp(key: string) { 48 | return transformOriginProps.has(key) 49 | } 50 | 51 | /** 52 | * Split values between style and transform keys. 53 | */ 54 | export function splitValues(variant: Variant) { 55 | const transform = {} 56 | const style = {} 57 | 58 | Object.entries(variant).forEach(([key, value]) => { 59 | // @ts-expect-error - Fix errors later for typescript 5 60 | if (isTransformProp(key) || isTransformOriginProp(key)) 61 | transform[key] = value 62 | // @ts-expect-error - Fix errors later for typescript 5 63 | else style[key] = value 64 | }) 65 | 66 | return { transform, style } 67 | } 68 | 69 | export function variantToStyle(variant: Variant) { 70 | // Split values between `transform` and `style` 71 | const { transform: _transform, style: _style } = splitValues(variant) 72 | 73 | // Generate transform string 74 | const { transform } = reactiveTransform(_transform) 75 | 76 | // Generate style string 77 | const { style } = reactiveStyle(_style) 78 | 79 | // @ts-expect-error - Set transform from style 80 | if (transform.value) 81 | style.value.transform = transform.value 82 | 83 | return style.value 84 | } 85 | -------------------------------------------------------------------------------- /src/types/variants.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'vue' 2 | import type { ResolvedSingleTarget, Transition } from './transitions' 3 | 4 | /** 5 | * Permissive properties keys 6 | */ 7 | export type PropertiesKeys = Record 8 | 9 | /** 10 | * SVG Supported properties 11 | */ 12 | export interface SVGPathProperties { 13 | pathLength?: number 14 | pathOffset?: number 15 | pathSpacing?: number 16 | } 17 | 18 | /** 19 | * Transform properties 20 | */ 21 | export type TransformValue = string | number 22 | 23 | export interface TransformProperties { 24 | x?: TransformValue | TransformValue[] 25 | y?: TransformValue | TransformValue[] 26 | z?: TransformValue | TransformValue[] 27 | translateX?: TransformValue | TransformValue[] 28 | translateY?: TransformValue | TransformValue[] 29 | translateZ?: TransformValue | TransformValue[] 30 | rotate?: TransformValue | TransformValue[] 31 | rotateX?: TransformValue | TransformValue[] 32 | rotateY?: TransformValue | TransformValue[] 33 | rotateZ?: TransformValue | TransformValue[] 34 | scale?: TransformValue | TransformValue[] 35 | scaleX?: TransformValue | TransformValue[] 36 | scaleY?: TransformValue | TransformValue[] 37 | scaleZ?: TransformValue | TransformValue[] 38 | skew?: TransformValue | TransformValue[] 39 | skewX?: TransformValue | TransformValue[] 40 | skewY?: TransformValue | TransformValue[] 41 | originX?: TransformValue | TransformValue[] 42 | originY?: TransformValue | TransformValue[] 43 | originZ?: TransformValue | TransformValue[] 44 | perspective?: TransformValue | TransformValue[] 45 | transformPerspective?: TransformValue | TransformValue[] 46 | } 47 | 48 | /** 49 | * Relevant styling properties 50 | */ 51 | export type StyleProperties = Omit 52 | 53 | /** 54 | * Available properties for useMotion variants 55 | */ 56 | export type MotionProperties = StyleProperties | TransformProperties | SVGPathProperties 57 | 58 | /** 59 | * Permissive properties for useSpring 60 | */ 61 | export type PermissiveMotionProperties = MotionProperties & Record 62 | 63 | /** 64 | * Variant 65 | */ 66 | export type Variant = { 67 | transition?: Transition 68 | } & MotionProperties 69 | 70 | /** 71 | * Motion variants object 72 | */ 73 | export type MotionVariants = { 74 | // Initial variant 75 | initial?: Variant 76 | // Lifecycle hooks variants 77 | enter?: Variant 78 | leave?: Variant 79 | // Intersection observer variants 80 | visible?: Variant 81 | visibleOnce?: Variant 82 | // Event listeners variants 83 | hovered?: Variant 84 | tapped?: Variant 85 | focused?: Variant 86 | } & { 87 | // Custom variants 88 | [key in T]?: Variant 89 | } 90 | -------------------------------------------------------------------------------- /src/presets/slide.ts: -------------------------------------------------------------------------------- 1 | import type { MotionVariants } from '../types' 2 | 3 | // Slide from left 4 | 5 | export const slideLeft: MotionVariants = { 6 | initial: { 7 | x: -100, 8 | opacity: 0, 9 | }, 10 | enter: { 11 | x: 0, 12 | opacity: 1, 13 | }, 14 | } 15 | 16 | export const slideVisibleLeft: MotionVariants = { 17 | initial: { 18 | x: -100, 19 | opacity: 0, 20 | }, 21 | visible: { 22 | x: 0, 23 | opacity: 1, 24 | }, 25 | } 26 | 27 | export const slideVisibleOnceLeft: MotionVariants = { 28 | initial: { 29 | x: -100, 30 | opacity: 0, 31 | }, 32 | visibleOnce: { 33 | x: 0, 34 | opacity: 1, 35 | }, 36 | } 37 | 38 | // Slide from right 39 | 40 | export const slideRight: MotionVariants = { 41 | initial: { 42 | x: 100, 43 | opacity: 0, 44 | }, 45 | enter: { 46 | x: 0, 47 | opacity: 1, 48 | }, 49 | } 50 | 51 | export const slideVisibleRight: MotionVariants = { 52 | initial: { 53 | x: 100, 54 | opacity: 0, 55 | }, 56 | visible: { 57 | x: 0, 58 | opacity: 1, 59 | }, 60 | } 61 | 62 | export const slideVisibleOnceRight: MotionVariants = { 63 | initial: { 64 | x: 100, 65 | opacity: 0, 66 | }, 67 | visibleOnce: { 68 | x: 0, 69 | opacity: 1, 70 | }, 71 | } 72 | 73 | // Slide from top 74 | 75 | export const slideTop: MotionVariants = { 76 | initial: { 77 | y: -100, 78 | opacity: 0, 79 | }, 80 | enter: { 81 | y: 0, 82 | opacity: 1, 83 | }, 84 | } 85 | 86 | export const slideVisibleTop: MotionVariants = { 87 | initial: { 88 | y: -100, 89 | opacity: 0, 90 | }, 91 | visible: { 92 | y: 0, 93 | opacity: 1, 94 | }, 95 | } 96 | 97 | export const slideVisibleOnceTop: MotionVariants = { 98 | initial: { 99 | y: -100, 100 | opacity: 0, 101 | }, 102 | visibleOnce: { 103 | y: 0, 104 | opacity: 1, 105 | }, 106 | } 107 | 108 | // Slide from bottom 109 | 110 | export const slideBottom: MotionVariants = { 111 | initial: { 112 | y: 100, 113 | opacity: 0, 114 | }, 115 | enter: { 116 | y: 0, 117 | opacity: 1, 118 | }, 119 | } 120 | 121 | export const slideVisibleBottom: MotionVariants = { 122 | initial: { 123 | y: 100, 124 | opacity: 0, 125 | }, 126 | visible: { 127 | y: 0, 128 | opacity: 1, 129 | }, 130 | } 131 | 132 | export const slideVisibleOnceBottom: MotionVariants = { 133 | initial: { 134 | y: 100, 135 | opacity: 0, 136 | }, 137 | visibleOnce: { 138 | y: 0, 139 | opacity: 1, 140 | }, 141 | } 142 | 143 | export default { 144 | slideLeft, 145 | slideVisibleLeft, 146 | slideVisibleOnceLeft, 147 | slideRight, 148 | slideVisibleRight, 149 | slideVisibleOnceRight, 150 | slideTop, 151 | slideVisibleTop, 152 | slideVisibleOnceTop, 153 | slideBottom, 154 | slideVisibleBottom, 155 | slideVisibleOnceBottom, 156 | } 157 | -------------------------------------------------------------------------------- /src/types/instance.ts: -------------------------------------------------------------------------------- 1 | import type { VueInstance } from '@vueuse/core' 2 | import type { MaybeRef, Ref, UnwrapRef } from 'vue' 3 | import type { MotionProperties, MotionVariants, Variant } from './variants' 4 | 5 | export type PermissiveTarget = VueInstance | MotionTarget 6 | 7 | export type MotionTarget = HTMLElement | SVGElement | null | undefined 8 | 9 | export interface MotionInstance> extends MotionControls { 10 | target: MaybeRef 11 | variants: MaybeRef 12 | variant: Ref 13 | state: Ref 14 | motionProperties: UnwrapRef 15 | } 16 | 17 | export interface UseMotionOptions { 18 | syncVariants?: boolean 19 | lifeCycleHooks?: boolean 20 | visibilityHooks?: boolean 21 | eventListeners?: boolean 22 | } 23 | 24 | export interface MotionControls> { 25 | /** 26 | * Apply a variant declaration and execute the resolved transitions. 27 | * 28 | * @param variant 29 | * @returns Promise 30 | */ 31 | apply: (variant: Variant | keyof V) => Promise | undefined 32 | /** 33 | * Apply a variant declaration without transitions. 34 | * 35 | * @param variant 36 | */ 37 | set: (variant: Variant | keyof V) => void 38 | /** 39 | * Stop all the ongoing transitions for the current element. 40 | */ 41 | stop: (keys?: string | string[]) => void 42 | /** 43 | * Helper to be passed to leave event. 44 | * 45 | * @param done 46 | */ 47 | leave: (done: () => void) => void 48 | /** 49 | * Computed reference reactive to the animation state of motion controls. 50 | */ 51 | isAnimating: any 52 | } 53 | 54 | export interface SpringControls { 55 | /** 56 | * Apply new values with transitions. 57 | * 58 | * @param variant 59 | */ 60 | set: (properties: MotionProperties) => void 61 | /** 62 | * Stop all transitions. 63 | * 64 | * @param variant 65 | */ 66 | stop: (key?: string | string[]) => void 67 | /** 68 | * Object containing all the current values of the spring. 69 | * 70 | * @param 71 | */ 72 | values: MotionProperties 73 | } 74 | 75 | export type MotionInstanceBindings> = Record> 76 | 77 | declare module 'vue' { 78 | export interface ComponentCustomProperties { 79 | $motions?: MotionInstanceBindings 80 | } 81 | } 82 | 83 | declare module '@vue/runtime-dom' { 84 | interface HTMLAttributes { 85 | variants?: MotionVariants 86 | // Initial variant 87 | initial?: Variant 88 | // Lifecycle hooks variants 89 | enter?: Variant 90 | leave?: Variant 91 | // Intersection observer variants 92 | visible?: Variant 93 | visibleOnce?: Variant 94 | // Event listeners variants 95 | hovered?: Variant 96 | tapped?: Variant 97 | focused?: Variant 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /docs/content/3.api/2.use-spring.md: -------------------------------------------------------------------------------- 1 | # useSpring 2 | 3 | useSpring is a simpler hook than [useMotion](/api/use-motion) and makes it possible to animate HTML or SVG Elements in your Vue components using spring animations. 4 | 5 | Spring animations often feel more natural and fluid compared to linear animations, as they are based on the physics of a spring in the real world. 6 | 7 | Springs maintain continuity for both static cases and cases with an initial velocity. This allows spring animations to adapt smoothly to user interactions like gestures. 8 | 9 | useSpring can be bound to a HTML or SVG element, or to a simple object. 10 | 11 | It skips the [Variants](/features/variants) system, allowing it to be as performant as using Popmotion natively, but with a nicer **API** to play with Vue refs. 12 | 13 | ## Parameters 14 | 15 | ### `values` 16 | 17 | The values argument expects a `motionProperties` object, which can be created using the [useMotionProperties](/api/use-motion-properties) function. 18 | 19 | ### `spring` 20 | 21 | Spring animations can be configured in multiple ways, using spring options. The most intuitive way is using `duration` and `bounce`. Alternatively, you can use `stiffness`, `mass`, and `damping` to configure a spring animation. 22 | 23 | Under the hood, `useSpring` uses **Popmotion**. See [Spring options in Popmotion](https://popmotion.io/#quick-start-animation-animate-spring-options) for a full list of spring options. 24 | 25 | Passing a `string` argument is optional. A default spring will be applied if you do not specify it. 26 | 27 | ## Exposed 28 | 29 | ### `values` 30 | 31 | Values are an object representing the current state from your spring animations. 32 | 33 | ### `set` 34 | 35 | Set is function allowing you to mutate the values with the transition specified as spring parameter. 36 | 37 | ### `stop` 38 | 39 | Stop is a function allowing you to stop all the ongoing animations for the spring. 40 | 41 | ## Example 42 | 43 | In the example below, click the green box to animate it, or press the escape key to stop the animation. 44 | 45 | ```html 46 | 51 | 52 | 75 | 76 | 92 | 93 | ``` 94 | -------------------------------------------------------------------------------- /src/features/eventListeners.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from '@vueuse/core' 2 | import { computed, ref, unref, watch } from 'vue' 3 | import type { MotionInstance, MotionVariants } from '../types' 4 | import { supportsMouseEvents, supportsPointerEvents, supportsTouchEvents } from '../utils/events' 5 | 6 | export function registerEventListeners>({ target, state, variants, apply }: MotionInstance) { 7 | const _variants = unref(variants) 8 | 9 | // State 10 | const hovered = ref(false) 11 | const tapped = ref(false) 12 | const focused = ref(false) 13 | 14 | const mutableKeys = computed(() => { 15 | let result: string[] = [...Object.keys(state.value || {})] 16 | 17 | if (!_variants) 18 | return result 19 | 20 | if (_variants.hovered) 21 | result = [...result, ...Object.keys(_variants.hovered)] 22 | 23 | if (_variants.tapped) 24 | result = [...result, ...Object.keys(_variants.tapped)] 25 | 26 | if (_variants.focused) 27 | result = [...result, ...Object.keys(_variants.focused)] 28 | 29 | return result 30 | }) 31 | 32 | const computedProperties = computed(() => { 33 | const result: Partial = {} 34 | 35 | Object.assign(result, state.value) 36 | 37 | if (hovered.value && _variants.hovered) 38 | Object.assign(result, _variants.hovered) 39 | 40 | if (tapped.value && _variants.tapped) 41 | Object.assign(result, _variants.tapped) 42 | 43 | if (focused.value && _variants.focused) 44 | Object.assign(result, _variants.focused) 45 | 46 | for (const key in result) { 47 | if (!mutableKeys.value.includes(key)) 48 | delete result[key] 49 | } 50 | 51 | return result 52 | }) 53 | 54 | // Hovered 55 | if (_variants.hovered) { 56 | useEventListener(target as any, 'mouseenter', () => (hovered.value = true)) 57 | useEventListener(target as any, 'mouseleave', () => { 58 | hovered.value = false 59 | tapped.value = false 60 | }) 61 | } 62 | 63 | // Tapped 64 | if (_variants.tapped) { 65 | // Mouse 66 | if (supportsMouseEvents()) { 67 | useEventListener(target as any, 'mousedown', () => (tapped.value = true)) 68 | useEventListener(target as any, 'mouseup', () => (tapped.value = false)) 69 | } 70 | 71 | // Pointer 72 | if (supportsPointerEvents()) { 73 | useEventListener(target as any, 'pointerdown', () => (tapped.value = true)) 74 | useEventListener(target as any, 'pointerup', () => (tapped.value = false)) 75 | } 76 | 77 | // Touch 78 | if (supportsTouchEvents()) { 79 | useEventListener(target as any, 'touchstart', () => (tapped.value = true)) 80 | useEventListener(target as any, 'touchend', () => (tapped.value = false)) 81 | } 82 | } 83 | 84 | // Focused 85 | if (_variants.focused) { 86 | useEventListener(target as any, 'focus', () => (focused.value = true)) 87 | useEventListener(target as any, 'blur', () => (focused.value = false)) 88 | } 89 | 90 | // Watch event states, apply it computed properties 91 | watch([hovered, tapped, focused], () => { 92 | apply(computedProperties.value) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /docs/components/content/Features.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 52 | 53 | 128 | 129 | 134 | -------------------------------------------------------------------------------- /src/utils/transform-parser.ts: -------------------------------------------------------------------------------- 1 | import type { MotionProperties, ResolvedValueTarget, TransformProperties } from '../types' 2 | 3 | /** 4 | * Return an object from a transform string. 5 | */ 6 | export function parseTransform(transform: string): Partial { 7 | // Split transform string. 8 | const transforms = transform.trim().split(/\) |\)/) 9 | 10 | // Handle "initial", "inherit", "unset". 11 | if (transforms.length === 1) 12 | return {} 13 | 14 | const parseValues = (value: string): string | number => { 15 | // If value is ending with px or deg, return it as a number 16 | if (value.endsWith('px') || value.endsWith('deg')) 17 | return Number.parseFloat(value) 18 | 19 | // Return as number 20 | if (Number.isNaN(Number(value))) 21 | return Number(value) 22 | 23 | // Parsing impossible, return as string 24 | return value 25 | } 26 | 27 | // Reduce the result to an object and return it 28 | return transforms.reduce((acc, transform: string) => { 29 | if (!transform) 30 | return acc 31 | 32 | const [name, transformValue] = transform.split('(') 33 | 34 | const valueArray = transformValue.split(',') 35 | 36 | const values = valueArray.map((val) => { 37 | return parseValues(val.endsWith(')') ? val.replace(')', '') : val.trim()) 38 | }) 39 | 40 | const value = values.length === 1 ? values[0] : values 41 | 42 | return { 43 | ...acc, 44 | [name]: value, 45 | } 46 | }, {}) 47 | } 48 | 49 | /** 50 | * Sets the state from a parsed transform string. 51 | * 52 | * Used in useElementTransform init to restore element transform string in cases it does exists. 53 | * 54 | * @param state 55 | * @param transform 56 | */ 57 | export function stateFromTransform(state: TransformProperties, transform: string) { 58 | Object.entries(parseTransform(transform)).forEach(([key, value]) => { 59 | // Axes reference for loops 60 | const axes = ['x', 'y', 'z'] 61 | 62 | // Handle translate3d and scale3d 63 | if (key === 'translate3d') { 64 | if (value === 0) { 65 | // @ts-expect-error - Fix errors later for typescript 5 66 | axes.forEach(axis => (state[axis] = 0)) 67 | return 68 | } 69 | 70 | // Loop on parsed scale / translate definition 71 | // @ts-expect-error - Fix errors later for typescript 5 72 | value.forEach((axisValue: ResolvedValueTarget, index: number) => (state[axes[index]] = axisValue)) 73 | 74 | return 75 | } 76 | 77 | // Get value w/o unit, as unit is applied later on 78 | value = Number.parseFloat(`${value}`) 79 | 80 | // Sync translateX on X 81 | if (key === 'translateX') { 82 | state.x = value 83 | return 84 | } 85 | 86 | // Sync translateY on Y 87 | if (key === 'translateY') { 88 | state.y = value 89 | return 90 | } 91 | 92 | // Sync translateZ on Z 93 | if (key === 'translateZ') { 94 | state.z = value 95 | return 96 | } 97 | 98 | // Set raw value 99 | // @ts-expect-error - Fix errors later for typescript 5 100 | state[key] = value 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /docs/content/2.features/7.components.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | vueuse/motion allows you to implement your animations directly within the template of your components without the need to wrap target elements in any additional components. 4 | 5 | These components work similar to the directive `v-motion` usage but work better in projects using server-side rendering. 6 | 7 | ## `` 8 | 9 | Example of a `` component using a motion preset on a `

` element: 10 | 11 | ```vue 12 | 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 | 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 | 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 | [![npm](https://img.shields.io/npm/v/@vueuse/motion.svg)](https://www.npmjs.com/package/@vueuse/motion) 4 | [![npm](https://img.shields.io/npm/v/vueuse-motion-nightly.svg)](https://www.npmjs.com/package/vueuse-motion-nightly) 5 | [![npm](https://img.shields.io/npm/dm/@vueuse/motion.svg)](https://npm-stat.com/charts.html?package=@vueuse/motion) 6 | [![minzip size](https://img.shields.io/bundlephobia/minzip/%40vueuse%2Fmotion/3)](https://www.npmjs.com/package/@vueuse/motion) 7 | [![Netlify Status](https://api.netlify.com/api/v1/badges/ab1db459-8420-4bc6-9fac-2bc247fa2385/deploy-status)](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 | 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 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------