web-animation-framer-motion-9dce52 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-animation-framer-motion-9dce52 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide:motion.*components takeinitial,animate,exitandtransitionprops; variants lift those states into named sets a parent can orchestrate;AnimatePresenceis what keeps a removed component mounted long enough to animate out;layoutandlayoutIdrun FLIP animations over layout changes;useScrollanduseInViewdrive motion from scroll position. Animatex,y,scale,rotateandopacity, and setMotionConfig reducedMotion="user"at the root.
Import:import { motion } from "motion/react"— the package was renamed fromframer-motionat v11.
Detailed Resources:
layout, layoutId, expandable cards, tab indicatorsuseAnimation chains, keyframe arrays, stagger() shapingpathLength drawing effectsAnimatePresence around itand a key on it; follow examples/core.md.
layout measures before andafter and animates the difference; follow examples/layout.md.
layoutId makes onetravel into the other; follow examples/layout.md.
useScroll with useTransform, or whileInView for aone-shot trigger; follow examples/scroll.md.
useAnimation gives the imperative handle; follow examples/sequences.md.
<critical_requirements>
Wrap anything that animates on removal in `AnimatePresence`. React unmounts the node the instant its condition goes false, so exit has nothing left to run against without the wrapper holding the node in the tree until the animation finishes.
Give every direct child of `AnimatePresence` a stable, unique `key`. The key is what AnimatePresence matches the departing element against; an index key re-points to a different item when the list changes and animates the wrong element out.
Animate `x`, `y`, `scale`, `rotate` and `opacity`. Motion writes these to transform and opacity, which the compositor owns; height, width and marginTop re-run layout on each frame instead.
Set `MotionConfig reducedMotion="user"` at the app root. Transform and layout animations then disable themselves for users who asked their OS for reduced motion, while opacity and colour keep working — so state changes stay legible rather than becoming instant.
</critical_requirements>
Auto-detection: motion/react, framer-motion, motion.div, AnimatePresence, MotionConfig, LayoutGroup, LazyMotion, layoutId, whileHover, whileTap, whileDrag, whileInView, useAnimation, useScroll, useTransform, useMotionValue, useSpring, useInView, usePageInView, useReducedMotion, useDragControls, staggerChildren, delayChildren, Variants
Applies to:
Handled elsewhere:
or moves. The styling layer settles those without a component wrapper
component states
<philosophy>
Motion animates state rather than time. A component declares what it looks like in each state, and Motion works out the interpolation, the interruption and the velocity carried across it — which is why a spring interrupted mid-flight continues from where it was rather than restarting.
</philosophy>
<decision_framework>
Motion is worth the component wrapper and the bundle when at least one is true:
interrupted spring
None of those true means the motion is a state change with both ends known in advance, which needs no runtime.
Reach for variants once more than one element shares the animation, or once the parent needs to control child timing — staggerChildren and delayChildren only exist on the variant path, and children inherit the parent's variant name without being passed anything. A single element with two states is clearer with initial/animate written inline.
</decision_framework>
<patterns>
Any HTML or SVG tag prefixed with motion. accepts initial, animate, exit and transition.
import { motion } from "motion/react";
export const FadeIn = ({ children }: { children: React.ReactNode }) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);y compiles to a transform; marginTop or top would relayout each frame.
Full code: examples/core.md
Variants name animation states so a parent can drive its children by name and control their timing.
import { motion, type Variants } from "motion/react";
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};Children inherit the parent's current variant name, so the item elements carry no animate prop of their own. staggerDirection: -1 reverses the cascade on exit, so a list unwinds from the bottom.
Full code: examples/core.md
Keeps a removed component in the tree until its exit animation finishes.
import { AnimatePresence, motion } from "motion/react";
<AnimatePresence>
{isOpen && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
/>
)}
</AnimatePresence>mode="sync" (default) overlaps the exit and the enter; mode="wait" holds the enter until the exit finishes, which is what page transitions want; mode="popLayout" takes the exiting element out of flow so the remaining siblings animate into place.
Full code: examples/core.md
whileHover, whileTap, whileFocus and whileDrag describe a state that holds for the duration of the gesture and unwinds when it ends.
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
/>Drag adds drag, dragConstraints (a ref or a pixel box), dragElastic for resistance past the bounds, and useDragControls when something other than the element itself starts the drag.
Full code: examples/core.md
layout measures the element before and after a React commit and animates the difference, so a change to flex order, grid placement or content size animates without any from-value being authored.
<motion.div layout transition={LAYOUT_SPRING}>
<motion.h2 layout="position">Title</motion.h2>
</motion.div>
{activeTab === tab && <motion.div layoutId="indicator" />}layout="position" animates a child's position but not its scale, which is what keeps text from stretching while the parent resizes. layoutId matches two elements that are never mounted at once and animates one into the other; the id is global, so LayoutGroup id={...} scopes it when the component can appear more than once on a page.
Full code: examples/layout.md
whileInView fires once on entry; useScroll with useTransform maps continuous scroll progress onto a value.
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
/>;
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
const y = useTransform(scrollYProgress, [0, 1], [-100, 100]);The value returned by useScroll is a motion value, which updates outside React — no re-render runs per scroll frame.
Full code: examples/scroll.md
// Springs: physics, no fixed duration, survive interruption
const BOUNCY = { type: "spring", stiffness: 300, damping: 10 };
const SNAPPY = { type: "spring", stiffness: 500, damping: 30 };
// Tweens: fixed duration and curve
const ENTER = { type: "tween", ease: "easeOut", duration: 0.3 };
const EXIT = { type: "tween", ease: "easeIn", duration: 0.2 };Springs suit anything a user can interrupt — buttons, cards, drags — because velocity carries across the interruption. Tweens suit motion that has to finish in a known time, such as a modal or a page change coordinated with something else.
Presets: reference.md
For animations triggered by something React state does not represent — a timer, a response, an error.
const controls = useAnimation();
useEffect(() => {
if (hasError) {
controls.start({ x: [0, -10, 10, -10, 0], transition: { duration: 0.3 } });
}
}, [hasError, controls]);
<motion.div animate={controls}>{children}</motion.div>;Passing an array to controls.start runs the steps in sequence, each awaiting the last.
Full code: examples/sequences.md
// Whole app
<MotionConfig reducedMotion="user">{children}</MotionConfig>
// One component, where the reduced form is different rather than absent
const shouldReduceMotion = useReducedMotion();
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0.2 : 0.5 }}
/>reducedMotion="user" disables transform and layout animation while leaving opacity and colour alone, so the app still shows that something changed. Reach for useReducedMotion when the reduced form needs its own values rather than the transform simply being dropped.
Full code: examples/core.md
stagger() (v12+) goes on delayChildren, not staggerChildren — it returns a function that computes each child's delay, which is what lets the cascade start from the centre or run to an easing curve.
import { stagger, usePageInView } from "motion/react";
const transition = {
delayChildren: stagger(0.05, { from: "center", ease: "easeOut" }),
};
// from: "first" (default) | "center" | "last" | an index
const isPageVisible = usePageInView(); // v12.19+, true on the serverusePageInView reports tab visibility, which is what a looping animation should be gated on so it stops burning frames in a background tab.
Full code: examples/sequences.md
</patterns>
<red_flags>
Breaks at runtime:
exit on a component with no AnimatePresence above it — React removes the node first and theprop never runs — wrap the conditional
AnimatePresence — fragments take no key, so nothing istracked and every exit is skipped — give each element its own keyed conditional
key={index} on animated list children — the key re-points to a different item on insert or sort,and the wrong element animates out — key by a stable id
height, width, top, left, margin or padding — relayouts each frame — animatescale and x/y, with transformOrigin set where the growth should start
layout whose text children lack layout="position" — the children scale with thebox and the type visibly stretches — add the prop to each child
MotionConfig reducedMotion="user" at the root
Surprising behaviour:
layoutId is global, so two instances of the same component on one page fight over it — scope themwith LayoutGroup id={...}
mode="wait" serialises exit and enter, so the perceived delay is the sum of both durationswhileInView is configured by viewport, while useScroll is configured by offset — the twoprop names are not interchangeable
motion.path, motion.circle and their siblings animate; a plain <path> inside amotion.svg is inert
useInView returns false during server rendering, so the server markup is the hidden stateunless the initial state is set to visible
useMotionValueEvent when a sideeffect has to run
drag and layout on one element contest the same transform; disable layout during the dragshort placeholder string will not reveal
willChange set by hand competes with Motion's own layer management, which already promotes whatit is animating
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.