Animation

Physics-based springs, tweens, transitions, and gesture detection.

spring(initialValue, options)

Creates a physics-based spring animation. Springs produce natural, fluid motion by simulating a damped harmonic oscillator. Unlike time-based animations, springs respond dynamically when interrupted — you can redirect a spring mid-flight and it will smoothly change course.

import { spring } from 'what-framework';

// Create a spring starting at 0
const position = spring(0, {
  stiffness: 100,   // Spring tension (default: 100)
  damping: 10,      // Friction (default: 10)
  mass: 1,          // Weight of the object (default: 1)
  precision: 0.01,  // Settle threshold (default: 0.01)
});

// Animate to a new value
position.set(200);

// Read current animated value (reactive)
console.log(position.current());

// Check animation state
console.log(position.isAnimating());  // true while moving
console.log(position.velocity());     // current speed

The returned object exposes these reactive accessors and methods:

  • current() — the current animated value (reactive signal)
  • target() — the value the spring is heading toward
  • velocity() — the current velocity of the animation
  • isAnimating() — whether the spring is still in motion
  • set(target) — start animating toward a new target
  • stop() — halt the animation at its current position
  • snap(value) — immediately jump to a value (no animation)

Automatic Cleanup

When created inside a component, the spring's requestAnimationFrame loop is automatically cancelled when the component unmounts. No manual cleanup needed.

Example: Animated Counter

A counter that springs smoothly between values instead of jumping:

import { signal, effect } from 'what-framework';
import { spring } from 'what-framework';

function AnimatedCounter() {
  const count = signal(0);
  const display = spring(0, { stiffness: 120, damping: 14 });

  // When count changes, animate the display value
  effect(() => display.set(count()));

  return (
    <div>
      <span>{() => Math.round(display.current())}</span>
      <button onClick={() => count.set(c => c + 10)}>+10</button>
    </div>
  );
}

tween(from, to, options)

Creates a time-based animation that interpolates from one value to another over a fixed duration. Unlike springs, tweens have a predictable end time and use easing functions to control their curve.

import { tween, easings } from 'what-framework';

// Animate from 0 to 100 over 500ms
const anim = tween(0, 100, {
  duration: 500,                    // milliseconds (default: 300)
  easing: easings.easeOutCubic,     // easing function
  onUpdate: (value, t) => {},       // called each frame
  onComplete: () => {},             // called when finished
});

// Reactive accessors
anim.progress();     // 0 to 1, raw progress
anim.value();        // interpolated value (0 to 100)
anim.isAnimating();  // true while running

// Cancel mid-animation
anim.cancel();

Example: Progress Bar

Animate a progress bar from 0% to 100% with a bounce easing:

import { tween, easings } from 'what-framework';

function ProgressBar() {
  const bar = tween(0, 100, {
    duration: 1200,
    easing: easings.easeOutBounce,
  });

  return (
    <div class="progress-track">
      <div
        class="progress-fill"
        style={() => `width: ${bar.value()}%`}
      />
    </div>
  );
}

Easing Functions

The easings object provides a set of common easing curves. Each function maps an input t (0 to 1) to an output value:

import { easings } from 'what-framework';

// Available easings:
easings.linear          // constant speed
easings.easeInQuad      // accelerate from zero
easings.easeOutQuad     // decelerate to zero
easings.easeInOutQuad   // accelerate then decelerate
easings.easeInCubic     // stronger acceleration
easings.easeOutCubic    // stronger deceleration
easings.easeInOutCubic  // stronger ease in/out
easings.easeInElastic   // elastic wind-up
easings.easeOutElastic  // elastic overshoot
easings.easeOutBounce   // bouncing settle

Pass any easing to tween() or useTransition():

const anim = tween(0, 100, { easing: easings.easeOutBounce });

// Or use a custom easing function
const customEase = (t) => t * t * t;
const anim2 = tween(0, 100, { easing: customEase });

useTransition

Orchestrate state transitions with a progress signal. Useful for coordinating page transitions, view swaps, or multi-step animations where you need to know when the transition completes.

import { useTransition, easings } from 'what-framework';

const transition = useTransition({
  duration: 400,                  // ms (default: 300)
  easing: easings.easeOutCubic,   // easing curve
});

// Reactive state
transition.isTransitioning();  // true during animation
transition.progress();         // 0 to 1 (eased)

// Start a transition — returns a Promise
await transition.start(() => {
  // Callback runs when transition completes
  swapContent();
});

Example: Page Transition

import { useTransition } from 'what-framework';
import { signal } from 'what-framework';

function PageContainer() {
  const currentPage = signal('home');
  const fade = useTransition({ duration: 250 });

  async function navigate(page) {
    await fade.start(() => currentPage.set(page));
  }

  return (
    <div style={() => `opacity: ${1 - fade.progress()}`}>
      {() => renderPage(currentPage())}
    </div>
  );
}

useGesture

Attach multi-touch gesture handlers to an element. Supports drag, swipe, pinch, tap, and long press with velocity tracking. Works with both mouse and touch events.

import { useGesture } from 'what-framework';

const cardRef = { current: null };

const gesture = useGesture(cardRef, {
  onDragStart: ({ x, y }) => {},
  onDrag: ({ x, y, deltaX, deltaY, velocity }) => {},
  onDragEnd: ({ deltaX, deltaY, velocity }) => {},
  onSwipe: ({ direction, velocity }) => {},
  onPinch: ({ scale, centerX, centerY }) => {},
  onTap: ({ x, y }) => {},
  onLongPress: ({ x, y }) => {},
  preventDefault: false,
});

The returned state object provides reactive signals:

  • isDragging() — whether a drag is in progress
  • currentX(), currentY() — current pointer position
  • deltaX(), deltaY() — distance from drag start
  • velocity() — current velocity as { x, y } in px/sec

Example: Draggable Card

import { useGesture, spring } from 'what-framework';
import { effect } from 'what-framework';

function DraggableCard() {
  const ref = { current: null };
  const x = spring(0);
  const y = spring(0);

  useGesture(ref, {
    onDrag: ({ deltaX, deltaY }) => {
      x.snap(deltaX);
      y.snap(deltaY);
    },
    onDragEnd: () => {
      // Spring back to origin
      x.set(0);
      y.set(0);
    },
  });

  return (
    <div
      ref={ref}
      style={() => `transform: translate(${x.current()}px, ${y.current()}px)`}
      class="card"
    >
      Drag me!
    </div>
  );
}

Touch Events and Scrolling

By default, touch events use { passive: true } to avoid blocking scroll. Set preventDefault: true if you need to call e.preventDefault() in your handlers (e.g., for a draggable element that should not scroll the page).

useAnimatedValue

A React Native-inspired API that wraps a signal with animation methods. Useful when you need to drive multiple animations from a single value or interpolate between ranges.

import { useAnimatedValue } from 'what-framework';

const progress = useAnimatedValue(0);

// Spring to a target
progress.spring(100, { stiffness: 120, damping: 14 });

// Or tween with timing
progress.timing(100, { duration: 500 });

// Read the current value
progress.value();  // reactive

// Interpolate into a different range
const opacity = progress.interpolate(
  [0, 50, 100],    // input range
  [0, 0.5, 1]      // output range
);

// Use in JSX
<div style={() => `opacity: ${opacity()}`} />

CSS Transitions

For CSS-class-based animations, use createTransitionClasses() and cssTransition(). These follow the enter/exit pattern used by CSS animation libraries.

import { createTransitionClasses, cssTransition } from 'what-framework';

// Generate class names for a transition called "fade"
const classes = createTransitionClasses('fade');
// {
//   enter: 'fade-enter',
//   enterActive: 'fade-enter-active',
//   enterDone: 'fade-enter-done',
//   exit: 'fade-exit',
//   exitActive: 'fade-exit-active',
//   exitDone: 'fade-exit-done',
// }

// Apply the transition to an element
await cssTransition(element, 'fade', 'enter', 300);
// Adds fade-enter, then fade-enter-active, then fade-enter-done

Define matching CSS classes in your stylesheet:

/* CSS for fade transition */
.fade-enter { opacity: 0; }
.fade-enter-active { opacity: 1; transition: opacity 300ms; }
.fade-enter-done { opacity: 1; }
.fade-exit { opacity: 1; }
.fade-exit-active { opacity: 0; transition: opacity 300ms; }
.fade-exit-done { opacity: 0; }

Live Demo

Click the buttons below to see a physics-based spring animation in action. The black square animates to the target position with natural overshoot and settle.

Live Demo — Spring Animation