animaly
Documentation menu

Packages

React hooks

Motion values hold numbers and strings that change without re-rendering. The hooks create them, derive them, follow the scroll and start animations by hand.

Motion values

A motion value holds a number or string that changes without re-rendering the component. Pass it in style and the renderer writes it each frame.

Motion values in style
import { animate, motion, useMotionValue, useTransform } from "@animaly/react";
import { useEffect } from "react";

export default function Slider() {
  const x = useMotionValue(0);
  const opacity = useTransform(x, [0, 200], [0.3, 1]);

  useEffect(() => {
    const controls = animate(x, 200, { type: "spring", stiffness: 120 });
    return () => controls.stop();
  }, [x]);

  return <motion.div style={{ x, opacity, width: 40, height: 40, background: "#7c3aed" }} />;
}
Listen to a motion value
import { animate, useMotionValue, useMotionValueEvent } from "@animaly/react";
import { useEffect, useState } from "react";

export default function Counter() {
  const count = useMotionValue(0);
  const [label, setLabel] = useState("0");

  useMotionValueEvent(count, "change", (latest) => setLabel(String(Math.round(latest))));

  useEffect(() => {
    const controls = animate(count, 1280, { duration: 1.2, ease: "easeOut" });
    return () => controls.stop();
  }, [count]);

  return <span>{label} downloads</span>;
}
Follow a value with a spring
import { motion, useMotionValue, useSpring } from "@animaly/react";

export default function Follower() {
  const x = useMotionValue(0);
  const smooth = useSpring(x, { stiffness: 200, damping: 20 });

  return (
    <div onPointerMove={(event) => x.set(event.clientX)} style={{ height: 120 }}>
      <motion.div style={{ x: smooth, width: 24, height: 24, borderRadius: 12, background: "#db2777" }} />
    </div>
  );
}
Build a string from motion values
import { animate, motion, useMotionTemplate, useMotionValue } from "@animaly/react";
import { useEffect } from "react";

export default function Glow() {
  const blur = useMotionValue(0);
  const filter = useMotionTemplate`blur(${blur}px)`;

  useEffect(() => {
    const controls = animate(blur, 4, { duration: 0.6 });
    return () => controls.stop();
  }, [blur]);

  return <motion.div style={{ filter, width: 80, height: 80, background: "#7c3aed" }} />;
}
Skew with velocity
import { motion, useMotionValue, useTransform, useVelocity } from "@animaly/react";

export default function Skew() {
  const x = useMotionValue(0);
  const velocity = useVelocity(x);
  const skewX = useTransform(velocity, [-1000, 0, 1000], [-15, 0, 15]);

  return (
    <div onPointerMove={(event) => x.set(event.clientX)} style={{ height: 120 }}>
      <motion.div style={{ x, skewX, width: 60, height: 60, background: "#7c3aed" }} />
    </div>
  );
}

Scroll

useScroll returns scrollX, scrollY, scrollXProgress and scrollYProgress as motion values, with the same target, container and offset options as scroll.

A scroll progress bar
import { motion, useScroll } from "@animaly/react";

export default function ReadingProgress() {
  const { scrollYProgress } = useScroll();
  return (
    <motion.div
      style={{ scaleX: scrollYProgress, transformOrigin: "left", position: "fixed", top: 0, left: 0, right: 0, height: 3, background: "#7c3aed" }}
    />
  );
}
Scroll-linked values for one element
import { motion, useScroll, useTransform } from "@animaly/react";
import { useRef } from "react";

export default function Reveal() {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end end"] });
  const opacity = useTransform(scrollYProgress, [0, 1], [0, 1]);
  const y = useTransform(scrollYProgress, [0, 1], [60, 0]);

  return (
    <div style={{ height: "200vh", paddingTop: "120vh" }}>
      <motion.div ref={ref} style={{ opacity, y, height: 200, background: "#ede9fe" }} />
    </div>
  );
}
Know when an element is visible
import { useInView } from "@animaly/react";
import { useRef } from "react";

export default function Lazy() {
  const ref = useRef<HTMLDivElement>(null);
  const visible = useInView(ref, { once: true, margin: "200px" });
  return <div ref={ref}>{visible ? "Loaded" : "Waiting"}</div>;
}

Imperative animations

useAnimate returns a scope ref and an animate whose selectors only match inside that scope. Its animations stop when the component unmounts.

Imperative animations with useAnimate
import { useAnimate } from "@animaly/react";

export default function Shake() {
  const [scope, animate] = useAnimate();

  const shake = () => animate(scope.current, { x: [0, -8, 8, -6, 6, 0] }, { duration: 0.4 });

  return (
    <div ref={scope}>
      <button type="button" onClick={shake}>
        Wrong password
      </button>
    </div>
  );
}
Sequences scoped to a component
import { useAnimate } from "@animaly/react";
import { useEffect } from "react";

export default function Intro() {
  const [scope, animate] = useAnimate();

  useEffect(() => {
    animate([
      ["h2", { opacity: [0, 1], y: [16, 0] }],
      ["p", { opacity: [0, 1] }, { at: "-0.1" }],
    ]);
  }, [animate]);

  return (
    <div ref={scope}>
      <h2 style={{ opacity: 0 }}>Hello</h2>
      <p style={{ opacity: 0 }}>Selectors only match inside this component.</p>
    </div>
  );
}
Start animations from outside with useAnimationControls
import { motion, useAnimationControls } from "@animaly/react";

export default function Bell() {
  const controls = useAnimationControls();
  return (
    <div>
      <motion.span animate={controls} style={{ display: "inline-block" }}>
        🔔
      </motion.span>
      <button type="button" onClick={() => controls.start({ rotate: [0, -15, 15, -10, 10, 0] })}>
        Ring
      </button>
    </div>
  );
}
Cycle through states
import { motion, useCycle } from "@animaly/react";

export default function Shape() {
  const [shape, next] = useCycle({ borderRadius: 0 }, { borderRadius: 40 });
  return <motion.div animate={shape} onTap={() => next()} style={{ width: 80, height: 80, background: "#7c3aed" }} />;
}