animaly
Documentation menu

Packages

React

@animaly/react is motion's React API running on animaly: motion components, variants, gestures, AnimatePresence, MotionConfig and the hooks. Change the import and keep your components.

Install

It needs React 18 or 19 and brings @animaly/dom, @animaly/core and @animaly/motion with it.

Install
pnpm add @animaly/react
Switch an existing import
// Before
// import { motion, AnimatePresence } from "motion/react";

// After
import { AnimatePresence, motion } from "@animaly/react";

export { AnimatePresence, motion };

Animated styles are written by animaly's renderer in one pass per frame, not by a listener per value. A motion value passed in style is bound to the element and written in the same pass.

Motion components

Every HTML and SVG element has a motion version. initial sets the starting values, animate the target, and transition how to get there. When animate changes, the element animates from where it is.

Animate on mount
import { motion } from "@animaly/react";

export default function Card() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 24 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ type: "spring", stiffness: 300, damping: 24 }}
      style={{ width: 160, height: 100, borderRadius: 16, background: "#7c3aed" }}
    />
  );
}
Animate when state changes
import { motion } from "@animaly/react";
import { useState } from "react";

export default function Toggle() {
  const [on, setOn] = useState(false);
  return (
    <button
      type="button"
      onClick={() => setOn(!on)}
      style={{ width: 64, height: 36, borderRadius: 18, padding: 4, background: on ? "#7c3aed" : "#d4d4d8", border: 0 }}
    >
      <motion.span
        animate={{ x: on ? 28 : 0 }}
        transition={{ type: "spring", stiffness: 500, damping: 30 }}
        style={{ display: "block", width: 28, height: 28, borderRadius: 14, background: "#fff" }}
      />
    </button>
  );
}

Transitions

Transitions take motion's options: type, duration, ease, springs by stiffness and damping or by bounce and visualDuration, delay and repeat. A key per value overrides default.

A transition per value
import { motion } from "@animaly/react";

export default function Badge() {
  return (
    <motion.div
      initial={{ opacity: 0, scale: 0.6 }}
      animate={{ opacity: 1, scale: 1 }}
      transition={{
        default: { type: "spring", bounce: 0.4 },
        opacity: { duration: 0.2, ease: "linear" },
      }}
    >
      New
    </motion.div>
  );
}
Keyframes and repeats
import { motion } from "@animaly/react";

export default function Pulse() {
  return (
    <motion.div
      animate={{ scale: [1, 1.2, 1], opacity: [1, 0.6, 1] }}
      transition={{ duration: 1.2, repeat: Infinity, ease: "easeInOut" }}
      style={{ width: 16, height: 16, borderRadius: 8, background: "#db2777" }}
    />
  );
}

SVG

Draw an SVG path
import { motion } from "@animaly/react";

export default function Check() {
  return (
    <svg viewBox="0 0 24 24" width={48} height={48}>
      <motion.path
        d="M4 12 L10 18 L20 6"
        fill="none"
        stroke="#7c3aed"
        strokeWidth={2}
        initial={{ pathLength: 0 }}
        animate={{ pathLength: 1 }}
        transition={{ duration: 0.5, ease: "easeOut" }}
      />
    </svg>
  );
}

Your own components

motion.create wraps a component that passes its props, including ref and style, on to a DOM element.

Animate your own components
import { motion } from "@animaly/react";
import type { ComponentProps } from "react";

function Card(props: ComponentProps<"div">) {
  return <div {...props} style={{ ...props.style, padding: 16, borderRadius: 12, background: "#ede9fe" }} />;
}

const MotionCard = motion.create(Card);

export default function Example() {
  return (
    <MotionCard initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
      A plain component that forwards its props
    </MotionCard>
  );
}

Variants

Variants name sets of values. Children inherit the parent's variant labels, so one animate on the parent runs the whole tree, with staggerChildren, delayChildren, staggerDirection and when to order it.

Variants with staggered children
import { motion } from "@animaly/react";

const list = {
  hidden: {},
  visible: { transition: { staggerChildren: 0.06, delayChildren: 0.1 } },
};
const item = {
  hidden: { opacity: 0, y: 12 },
  visible: { opacity: 1, y: 0 },
};

export default function Menu() {
  return (
    <motion.ul initial="hidden" animate="visible" variants={list}>
      {["Profile", "Settings", "Billing", "Sign out"].map((label) => (
        <motion.li key={label} variants={item}>
          {label}
        </motion.li>
      ))}
    </motion.ul>
  );
}
Animate the parent before its children
import { motion } from "@animaly/react";

const panel = {
  closed: { height: 0, transition: { when: "afterChildren" } },
  open: { height: 160, transition: { when: "beforeChildren", staggerChildren: 0.05 } },
};
const row = { closed: { opacity: 0 }, open: { opacity: 1 } };

export default function Panel() {
  return (
    <motion.div initial="closed" animate="open" variants={panel} style={{ overflow: "hidden", background: "#ede9fe" }}>
      {[1, 2, 3].map((n) => (
        <motion.p key={n} variants={row}>
          Row {n}
        </motion.p>
      ))}
    </motion.div>
  );
}
Variants that depend on each child
import { motion } from "@animaly/react";

const item = {
  hidden: { opacity: 0, x: -20 },
  visible: (index: number) => ({ opacity: 1, x: 0, transition: { delay: index * 0.08 } }),
};

export default function Steps() {
  return (
    <ol>
      {["Install", "Import", "Animate"].map((step, index) => (
        <motion.li key={step} custom={index} initial="hidden" animate="visible" variants={item}>
          {step}
        </motion.li>
      ))}
    </ol>
  );
}

Exit animations

Wrap elements that can unmount in AnimatePresence and give them an exit. They stay in the DOM until their exit animation finishes. Each direct child needs a stable key.

Animate out before unmounting
import { AnimatePresence, motion } from "@animaly/react";
import { useState } from "react";

export default function Notice() {
  const [open, setOpen] = useState(true);
  return (
    <div>
      <button type="button" onClick={() => setOpen(!open)}>
        Toggle
      </button>
      <AnimatePresence>
        {open && (
          <motion.p
            key="notice"
            initial={{ opacity: 0, y: -8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
          >
            Saved.
          </motion.p>
        )}
      </AnimatePresence>
    </div>
  );
}
Remove items from a list
import { AnimatePresence, motion } from "@animaly/react";
import { useState } from "react";

export default function Todos() {
  const [items, setItems] = useState(["Write docs", "Ship 0.2", "Answer issues"]);
  return (
    <ul>
      <AnimatePresence onExitComplete={() => console.log("removed")}>
        {items.map((item) => (
          <motion.li
            key={item}
            exit={{ opacity: 0, x: 40 }}
            onClick={() => setItems(items.filter((other) => other !== item))}
          >
            {item}
          </motion.li>
        ))}
      </AnimatePresence>
    </ul>
  );
}

mode="wait" lets the old child finish leaving before the new one enters; popLayout and the default sync are supported as well. initial={false} skips the enter animation of the children present on first render.

Swap content one after the other
import { AnimatePresence, motion } from "@animaly/react";
import { useState } from "react";

const tabs = ["Overview", "Usage", "Billing"];

export default function Tabs() {
  const [tab, setTab] = useState(tabs[0]);
  return (
    <div>
      {tabs.map((name) => (
        <button key={name} type="button" onClick={() => setTab(name)}>
          {name}
        </button>
      ))}
      <AnimatePresence mode="wait" initial={false}>
        <motion.section
          key={tab}
          initial={{ opacity: 0, x: 16 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: -16 }}
          transition={{ duration: 0.15 }}
        >
          {tab}
        </motion.section>
      </AnimatePresence>
    </div>
  );
}

usePresence, useIsPresent and usePresenceData let a component run its own exit.

Run your own exit with usePresence
import { usePresence } from "@animaly/react";
import { useEffect } from "react";

export default function Toast() {
  const [isPresent, safeToRemove] = usePresence();

  useEffect(() => {
    if (isPresent) return;
    const timer = setTimeout(safeToRemove, 200);
    return () => clearTimeout(timer);
  }, [isPresent, safeToRemove]);

  return <p>Copied to clipboard</p>;
}

Gestures

whileHover, whileTap, whileFocus and whileInView animate while the gesture lasts and return when it ends. Their callbacks are onHoverStart, onHoverEnd, onTap, onTapStart, onTapCancel, onViewportEnter and onViewportLeave.

Hover and tap
import { motion } from "@animaly/react";

export default function Button() {
  return (
    <motion.button
      type="button"
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      transition={{ type: "spring", stiffness: 400, damping: 25 }}
      onTap={() => console.log("tapped")}
    >
      Save
    </motion.button>
  );
}
Focus
import { motion } from "@animaly/react";

export default function Field() {
  return (
    <motion.input
      placeholder="Email"
      whileFocus={{ scale: 1.02, boxShadow: "0 0 0 3px rgba(124, 58, 237, 0.4)" }}
      style={{ boxShadow: "0 0 0 0px rgba(124, 58, 237, 0)" }}
    />
  );
}
Animate when scrolled into view
import { motion } from "@animaly/react";

export default function Section() {
  return (
    <motion.section
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, amount: 0.4 }}
      onViewportEnter={() => console.log("seen")}
    >
      Features
    </motion.section>
  );
}

Motion values and hooks

useMotionValue, useTransform, useSpring, useScroll, useAnimate and the other hooks have their own page: React hooks.

MotionConfig and reduced motion

MotionConfig sets a default transition for everything inside it, along with reducedMotion, skipAnimations, isStatic, nonce and isValidProp. With reducedMotion="user", position and size values (x, y, width, height, top, left and the like) jump to their targets when the system asks for reduced motion, while opacity, color, scale and rotate still animate, as in motion.

Defaults for a whole subtree
import { MotionConfig, motion } from "@animaly/react";

export default function App() {
  return (
    <MotionConfig transition={{ type: "spring", stiffness: 300, damping: 26 }} reducedMotion="user">
      <motion.h1 initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
        Dashboard
      </motion.h1>
      <motion.div initial={{ x: -20 }} animate={{ x: 0 }}>
        Content
      </motion.div>
    </MotionConfig>
  );
}
Read the reduced motion setting
import { motion, useReducedMotion } from "@animaly/react";

export default function Hero() {
  const reduce = useReducedMotion();
  return <motion.div initial={{ opacity: 0, y: reduce ? 0 : 40 }} animate={{ opacity: 1, y: 0 }} />;
}

Server rendering

Components render their initial styles to HTML on the server, without a DOM, so the first paint already shows the starting state.

Server rendering
import { motion } from "@animaly/react";
import { renderToString } from "react-dom/server";

// The initial styles are part of the HTML, so nothing jumps when the page hydrates.
const html = renderToString(<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} />);

export default html;

Compatibility

The package is checked against motion v13.4.6: 342 of motion's own React tests, covering motion components, variants, gestures, AnimatePresence, MotionConfig and the hooks, run against it in Chromium, Firefox and WebKit.

Known differences

  • MotionValue.get() returns the live animated value at the moment you call it; motion returns the value of the last frame. A value read right after an animation starts is already slightly past its first keyframe.
  • Without MotionGlobalConfig.mix, colors must be hex, rgb(), rgba(), hsl() or hsla(); motion also accepts named colors such as red.

Not done yet

Layout animations (layout, layoutId, LayoutGroup), drag, Reorder, LazyMotion and m. Keep motion for components that need them.

Parts of motion are ported under its MIT license; the package's NOTICE file lists them. The API can still change before 1.0.