Skip to content

ktsn/css-spring-animation

Repository files navigation

CSS Spring Animation

English | 日本語

An intuitive and predictable spring animation library powered by CSS Transition. It is inspired by Animate with springs of WWDC 2023. The features of this library are:

  • Implementing spring animation with CSS Transition
  • Animation options are intuitive and predictable
    • bounce: Bounciness of an animation
    • duration: Perceptive animation duration
  • Graceful degradation with requestAnimationFrame for browsers that do not support the features used in the library

Getting Started

There is a Vue binding of the library. Install it with npm (or yarn, pnpm):

$ npm install @css-spring-animation/vue

When you use <script setup> in a single file component, you can use spring higher-order component as below:

<script setup>
import { ref } from 'vue'
import { spring } from '@css-spring-animation/vue'

const moved = ref(false)
</script>

<template>
  <button type="button" class="button" @click="moved = !moved">Toggle</button>

  <!-- render <div> element animating with style specified on :spring-style -->
  <spring.div
    class="rectangle"
    :spring-style="{
      translate: moved ? '100px' : '0px',
    }"
    :duration="600"
    :bounce="0.3"
  ></spring.div>
</template>

The property name after spring. is the tag name to be rendered. For example, <spring.div> renders <div> element. The element has the style specified on :spring-style prop. Spring animation will be triggered when the value of :spring-style prop is changed.

Bounce and Duration

bounce and duration options are used to specify the bounciness and perceptive duration of an animation.

bounce
Bounciness of an animation. The value is between -1 and 1. The default value is 0.

duration
Perceptive duration (ms) of an animation. The default value is 1000.

disabled and inferVelocity

<spring> component and useSpring composable accept a disabled option. While disabled is true, ongoing animation is stopped and further style changes update the value immediately without triggering a new animation.

When animation later resumes, the spring's initial velocity comes from one of two sources, controlled by inferVelocity:

  • inferVelocity: true (default) — velocity is inferred from the rate of recent style updates. Use this when you drive the element manually (e.g. by dragging) and want the spring to pick up momentum from your motion. See the Swipe demo: while the user drags, disabled is true, and on release the spring fires with the inferred drag velocity.
  • inferVelocity: false — velocity is left untouched, preserving the velocity of the previous spring animation. Use this when you want to teleport the element without disturbing momentum. See the Picker demo: scrolling the wheel wraps the picker around with disabled: true, inferVelocity: false, preserving the rotation momentum across the wrap.

inferVelocity only takes effect while disabled is true. When disabled is false, the animation owns both value and velocity and inferVelocity is ignored.

Spring Style Caveats

All numbers in a style value must have the same unit and must be appeared in the same order. For example, the following :spring-style value is invalid and will not work as expected:

<template>
  <!-- ❌ this example will not work as expected -->
  <spring.div
    :spring-style="{ transform: flag ? 'translate(100px, 100px)' : 'scale(2)' }"
  ></spring.div>
</template>

This is because the library parses the numbers in the style value, then calculate the animation for each number in the style value. The library cannot understand the meaning of translate, scale nor predict the difference between 100% and 100px. To fix the above example, you need to specify both translate and scale in the same order and always use the same unit:

<template>
  <!-- ✅ all numbers in the :spring-style have the same unit and are in the same order -->
  <spring.div
    :spring-style="{
      transform: flag
        ? 'scale(1) translate(100px, 100px)'
        : 'scale(2) translate(0, 0)',
    }"
  ></spring.div>
</template>

springValue and springComputed

springValue holds a number that lives inside an animating style. Assigning to its target automatically triggers an animation toward that value. Combined with the sv tagged template, it can be embedded inside a CSS value.

<script setup>
import { spring, springValue, sv } from '@css-spring-animation/vue'

const x = springValue(0)
const y = springValue(0)

function move() {
  // Assign the animation destination to `target`
  x.target = 200
  y.target = 100
}
</script>

<template>
  <button @click="move">Move</button>

  <!-- Embed spring values into the CSS value via `sv` -->
  <spring.div
    :spring-style="{ translate: sv`${x}px ${y}px` }"
    :duration="600"
    :bounce="0.3"
  />
</template>

You can also build CSS values with regular template literals over a Vue ref and the animation will still run. Using springValue instead lets you read the live value and velocity during the animation.

const x = springValue(0)

// Snapshot the live value and velocity at the time of the call (not reactive).
x.current()
x.velocity()

springComputed is the spring value counterpart of Vue's computed. Like computed, it derives a spring value from other reactive sources.

<script setup>
import { ref } from 'vue'
import { spring, springComputed, sv } from '@css-spring-animation/vue'

const offset = ref(0)

// Derive spring values from offset. `target` is read-only.
const x = springComputed(() => offset.value)
const y = springComputed(() => offset.value * 2)

function move() {
  offset.value = offset.value === 0 ? 100 : 0
}
</script>

<template>
  <button @click="move">Move</button>

  <spring.div
    :spring-style="{ translate: sv`${x}px ${y}px` }"
    :duration="600"
    :bounce="0.3"
  />
</template>

How It Works

The library sets spring animation expression in an animating CSS value including a custom property that representing elapsed time (let's say --t here). Then register --t by using CSS.registerProperty to be able to apply CSS Transition on it. The pseudo code of the spring animation expression looks like below:

// Register --t
CSS.registerProperty({
  name: '--t',
  syntax: '<number>',
  inherits: false,
  initialValue: 0,
})

// Set initial state
el.style.setProperty('--t', 0)

// Set spring animatioin expression including --t
el.style.translate = 'calc(P * (A * var(--t) + B) * exp(-C * var(--t)) - Q)'

// Re-render
requestAnimationFrame(() => {
  // Trigger animation
  el.style.setProperty('--t', 1)
  el.style.transition = '--t 1000ms linear'
})

The library also provides a graceful degradation for browsers that do not support CSS.registerProperty and exp() function of CSS. In this case, the library will use requestAnimationFrame to animate the style value instead of CSS Transition.

API Reference

<spring> component

It renders a native HTML element as same tag name as the property name (e.g. <spring.div> renders <div> element).

Props

  • spring-style: Style object to be animated
  • bounce
  • duration
  • disabled
  • inferVelocity
  • relocating (deprecated — use disabled + inferVelocity: false)

Events

  • spring-finish: Emitted when the animation completes visually (just after duration passes).
  • spring-settle: Emitted when the animation has fully settled (velocity decayed to zero).

Events fire per animation cycle on the latest cycle only. If spring-style is updated mid-animation, the previous cycle is interrupted and its events are suppressed. Events are also suppressed while disabled is set.

<script setup>
import { spring } from '@css-spring-animation/vue'

const position = ref(0)

function onFinish() {
  // ...
}
</script>

<template>
  <spring.div
    :spring-style="{
      translate: `${position.value}px`,
    }"
    :duration="600"
    :bounce="0.3"
    @spring-finish="onFinish"
  ></spring.div>
</template>

<SpringTransition> component

<SpringTransition> is a spring animation version of Vue's <Transition> component. It triggers animation from enter-from style to spring-style on entering and from spring-style to leave-to on leaving.

Props

  • spring-style: Default style of a child element.

  • enter-from: Style of a child element before entering.

  • leave-to: Style of a child element after leaving. Fallback to enter-from style if not specified.

  • bounce

  • duration

  • Inherited props from Vue's <Transition> component:

    • name
    • mode
    • enterFromClass
    • enterActiveClass
    • enterToClass
    • leaveFromClass
    • leaveActiveClass
    • leaveToClass

Events

  • before-enter
  • after-enter
  • enter-cancelled
  • before-leave
  • after-leave
  • leave-cancelled
<script setup>
import { ref } from 'vue'
import { SpringTransition } from '@css-spring-animation/vue'

const isShow = ref(false)
</script>

<template>
  <button type="button" class="button" @click="isShow = !isShow">Toggle</button>

  <!-- Trigger spring animation for the child element -->
  <SpringTransition
    :spring-style="{
      translate: '0',
    }"
    :enter-from="{
      translate: '-100px',
    }"
    :leave-to="{
      translate: '100px',
    }"
    :duration="600"
    :bounce="0"
  >
    <!-- .rectangle element will be animated when v-show value is changed -->
    <div v-show="isShow" class="rectangle"></div>
  </SpringTransition>
</template>

<SpringTransitionGroup> component

<SpringTransitionGroup> is a spring animation version of Vue's <TransitionGroup> component. It can have spring-style, enter-from and leave-to props as same as <SpringTransition>.

Props

  • spring-style: Default style of a child element.

  • enter-from: Style of a child element before entering.

  • leave-to: Style of a child element after leaving. Fallback to enter-from style if not specified.

  • bounce

  • duration

  • Inherited props from Vue's <TransitionGroup> component:

    • tag
    • name
    • enterFromClass
    • enterActiveClass
    • enterToClass
    • leaveFromClass
    • leaveActiveClass
    • leaveToClass

Events

  • before-enter
  • after-enter
  • enter-cancelled
  • before-leave
  • after-leave
  • leave-cancelled
<script setup>
import { SpringTransitionGroup } from '@css-spring-animation/vue'

const list = ref([
  // ...
])
</script>

<template>
  <!-- Trigger spring animation for the child elements -->
  <SpringTransitionGroup
    tag="ul"
    :spring-style="{
      opacity: 1,
    }"
    :enter-from="{
      opacity: 0,
    }"
    :leave-to="{
      opacity: 0,
    }"
    :duration="800"
    :bounce="0"
  >
    <!-- List items must have key prop -->
    <li v-for="item of list" :key="item.id">
      <!-- ... -->
    </li>
  </SpringTransitionGroup>
</template>

useSpring composable

Deprecated. Prefer the <spring> component. If you need access to realValue / realVelocity, use springValue instead.

A composable function to generate spring animation style. It also returns the real value and velocity of the corresponding number in the style value. They are as same shape as the style value except that its values are the array of numbers.

The first argument is a function or ref that returns the style object to be animated. The second argument is an options object. It also can be a function or ref that returns the options.

The options object expectes the following properties:

  • bounce
  • duration
  • disabled
  • inferVelocity
  • relocating (deprecated — use disabled + inferVelocity: false)

It is expected to be used in a complex situation that <spring> component is not suitable to be used.

<script setup>
import { ref } from 'vue'
import { useSpring } from '@css-spring-animation/vue'

const position = ref(0)

const { style, realValue, realVelocity } = useSpring(
  () => {
    return {
      translate: `${position.value}px`,
    }
  },
  () => {
    return {
      duration: 600,
      bounce: 0.3,
    }
  },
)
</script>

<template>
  <div :style="style"></div>
  <ul>
    <li>realValue: {{ realValue.translate[0] }}</li>
    <li>realVelocity: {{ realVelocity.translate[0] }}</li>
  </ul>
</template>

useSpring provides onFinishCurrent and onSettleCurrent functions for waiting until the current animation is finished or settled. You can register a callback function that will be called when an ongoing animation is finished/settled.

<script setup>
import { ref } from 'vue'
import { useSpring } from '@css-spring-animation/vue'

const position = ref(0)

const { style, onFinishCurrent } = useSpring(() => {
  return {
    translate: `${position.value}px`,
  }
})

function move() {
  // Move to 100px
  position.value = 100

  // Wait for the animation is finished (duration passed) triggered by the above position update
  onFinishCurrent(() => {
    // Move to 0px
    position.value = 0
  })

  // Wait for the animation is settled (visually stopped) triggered by the above position update
  onSettleCurrent(() => {
    console.log('settled')
  })
}
</script>

springValue(initial)

Creates a reactive holder for a single animated number. Pair with <spring> element via the sv tagged template.

Returns an object with:

  • target (number, reactive read/write) — animation destination. Assigning to it triggers an animation when bound to a <spring> element. Applied immediately while the bound element has disabled: true.
  • current(): number — non-reactive snapshot of the live animating value. While bound, reads through the spring element's value; otherwise returns target.
  • velocity(): number — non-reactive snapshot of the live velocity. While bound, reads the animation velocity; otherwise returns 0.
import { springValue } from '@css-spring-animation/vue'

const x = springValue(0)
x.target = 100 // trigger animation when bound
console.log(x.current(), x.velocity())

springComputed(getter)

The computed version of springValue. Like Vue's computed, it takes a getter that returns a number and produces a spring value. The shape is identical to a springValue except target is read-only and is automatically derived from the getter's reactive dependencies.

sv tagged template

A tagged template that builds a :spring-style value by embedding springValue / springComputed instances into a CSS expression.

v-spring-style and v-spring-options directivies

v-spring-style directive is used to specify the style to be animated. v-spring-options directive is used to specify the options of the animation.

It is expected to be used out of <script setup> where <spring> component is not able to be used.

You can register the directives by using plugin object exported as springDirectives:

import { createApp } from 'vue'
import App from './App.vue'
import { springDirectives } from '@css-spring-animation/vue'

createApp(App).use(springDirectives).mount('#app')

Then you can use the directives in a template:

<template>
  <div
    v-spring-style="{
      translate: `${position}px`,
    }"
    v-spring-options="{
      duration: 600,
      bounce: 0.3,
    }"
  ></div>
</template>

springCSS utility

A utility function that generates a CSS transition string with spring animation easing. This allows you to use spring animations with native CSS transitions.

Parameters

  • duration: Duration in milliseconds (required)
  • bounce: Bounciness (-1 to 1, default: 0)

Return value

Returns a CSS transition value string that can be used in the transition CSS property.

import { springCSS } from '@css-spring-animation/vue'

// Generate spring transition CSS
const transition = springCSS(400, 0.1)

// Use with DOM element directly
const element = document.querySelector('.my-element')
element.style.transition = `transform ${springCSS(600, 0.3)}`
element.style.transform = 'translateX(100px)'

// Use for multiple properties
element.style.transition = `
  transform ${springCSS(600, 0.3)},
  opacity ${springCSS(400, 0)}
`
element.style.transform = 'translateX(100px)'
element.style.opacity = '0.5'

springGenerator utility

A utility function that creates a spring iterator for manual animation control. It yields animation values based on elapsed time, useful for custom animation loops or non-DOM animations.

Parameters

  • from: Starting value (required)
  • to: Target value (required)
  • bounce: Bounciness (-1 to 1, default: 0)
  • duration: Duration in milliseconds (default: 1000)
  • velocity: Initial velocity in units per second (default: 0)

Return value

Returns a SpringGenerator object with a next(elapsedMs) method that returns { value: number, done: boolean }.

import { springGenerator } from '@css-spring-animation/vue'

// Create a spring iterator
const iter = springGenerator({
  from: 0,
  to: 100,
  bounce: 0.2,
  duration: 500,
})

// Get values at specific times
let result = iter.next(0) // { value: 0, done: false }
result = iter.next(100) // { value: ~80, done: false }
result = iter.next(1200) // { value: 100, done: true }
import { springGenerator } from '@css-spring-animation/vue'

// Example: Custom animation loop
const iter = springGenerator({ from: 0, to: 100, duration: 600 })
const startTime = performance.now()

function animate(now: number) {
  const elapsed = now - startTime
  const { value, done } = iter.next(elapsed)

  // Use the value for custom rendering
  console.log(value)

  if (!done) {
    requestAnimationFrame(animate)
  }
}

requestAnimationFrame(animate)

About

Intuitive and predictable spring animation library powered by CSS Transition

Resources

License

Stars

Watchers

Forks

Contributors

Languages