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.
pnpm add @animaly/react// 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.
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" }}
/>
);
}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.
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>
);
}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
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.
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.
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>
);
}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>
);
}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.
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>
);
}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.
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.
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.
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>
);
}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)" }}
/>
);
}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.
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>
);
}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.
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()orhsla(); motion also accepts named colors such asred.
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.