Popmotion: The Animation Engine Behind Framer Motion
Popmotion powers Framer Motion under the hood. Learn how to use it directly for physics-based animations, springs, and keyframes in JavaScript.
What Is Popmotion, Actually?
Most developers reach for Framer Motion without ever thinking about what's running underneath it. Popmotion is that engine — a functional animation library built in TypeScript that ships as the core of Framer Motion since version 5.0. It handles springs, keyframes, inertia, and decay animations. Framer wraps it in a friendly React API, but Popmotion itself has no opinion about frameworks.
The library was created by Matt Perry (the same person behind Framer Motion) back around 2017. It went through a serious rewrite for the v11 era — the API got leaner, tree-shaking became real, and the physics model got noticeably more accurate. If you've ever wondered why Framer Motion's spring animations feel different from CSS transition: all 0.3s ease, this is why.
Honestly, most React projects don't need Popmotion directly. You'd just use Framer Motion and be done with it. But there are real cases where dropping to the raw engine makes sense: animating Web Components, working in vanilla JS, animating values in a canvas renderer, or pairing it with libraries that have no Framer Motion integration.
Worth noting: Popmotion is tiny on its own. The core animate function adds roughly 5KB gzipped to your bundle. Compare that to importing all of Framer Motion at ~30KB — if you only need programmatic animation without React, Popmotion is worth a look.
Installing Popmotion and Your First Animation
Installation is a single line. Popmotion lives on npm as popmotion and has been stable on the v11 API since 2023:
npm install popmotionThe core export is animate. It takes a from, to, and an onUpdate callback. That callback fires on every animation frame with the current value. You wire it to whatever you want — a DOM element, a canvas, a Three.js object, a state variable. There's no magic binding layer. That's a feature, not a limitation.
import { animate } from 'popmotion';
const element = document.querySelector('.box') as HTMLElement;
animate({
from: 0,
to: 400,
duration: 600,
onUpdate: (value) => {
element.style.transform = `translateX(${value}px)`;
},
});That's it. No useEffect, no useRef, no JSX. If you're animating something outside of React — say, a popup in a browser extension, or a Figma plugin UI — this pattern is genuinely clean. In practice, you'll find the lack of abstraction liberating for those contexts.
Spring Physics: Where Popmotion Really Shines
CSS transitions give you easing curves. Popmotion gives you springs. The difference is physical — a spring-based animation responds to velocity, mass, stiffness, and damping. It can overshoot. It can bounce. It feels alive in a way that cubic-bezier(0.25, 0.1, 0.25, 1) never will.
import { animate } from 'popmotion';
animate({
from: 0,
to: 300,
type: 'spring',
stiffness: 400,
damping: 20,
mass: 1,
onUpdate: (v) => {
box.style.transform = `translateX(${v}px)`;
},
});The stiffness value controls how hard the spring pulls toward the target. A value of 400 is snappy — you'll feel it react. Drop it to 80 and the animation feels sluggish and elastic, like a rubber band. damping controls how quickly the oscillation dies out. Set damping to 0 and you get a bounce that never stops. Set it to 50+ and the spring barely overshoots at all.
Look, this is where most developers get confused: spring animations don't have a fixed duration. The motion ends when the velocity and distance-to-target fall below a threshold. That's intentional. It means a spring animation interrupted halfway through will react from its current velocity, not snap back to zero. That's why gesture-driven UIs feel natural when built on springs — the Framer Motion advanced guide covers this exact behavior in the context of drag and layout animations.
Quick aside: if you do need a fixed duration, Popmotion has a duration property that will modify the spring to complete within that time. It approximates rather than fully constrains, but it gets you close enough for UI work.
Keyframes, Sequences, and Orchestrating Multiple Values
Springs aren't always what you want. Sometimes you need a choreographed sequence — a value that hits multiple waypoints in order. Popmotion handles this with keyframe arrays in the to property:
import { animate } from 'popmotion';
animate({
from: 0,
to: [0, 200, 150, 300],
duration: 1200,
ease: ['easeIn', 'easeOut', 'linear'],
onUpdate: (v) => {
el.style.opacity = String(v / 300);
},
});The ease array maps to the segments between keyframes, not to the whole animation. So in the example above, easeIn runs from keyframe 0 to 1, easeOut from 1 to 2, and linear from 2 to 3. You need one fewer easing value than you have keyframes. Miss that and you'll get a runtime error.
For multiple values in parallel, just call animate multiple times. There's no built-in timeline — but honestly, for UI animations, you rarely need one. The Framer Motion advanced patterns in the context of useAnimate show how to sequence animations using async/await, which works cleanly on top of Popmotion's Promise-based API too.
Popmotion's animate returns a PlaybackControls object with .stop(), .pause(), and .resume() methods. That's your escape hatch when an animation needs to be cancelled mid-flight — like when a user navigates away or closes a modal before the entrance animation completes.
Using Popmotion Without React — Real-World Patterns
The canvas use case is probably the most compelling reason to reach for Popmotion directly rather than through Framer Motion. Canvas doesn't have DOM nodes — there's nothing to pass a motion.div to. But you can absolutely drive canvas render loops with Popmotion values.
import { animate } from 'popmotion';
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
let ballX = 50;
animate({
from: 50,
to: 550,
type: 'spring',
stiffness: 200,
damping: 15,
onUpdate: (x) => {
ballX = x;
ctx.clearRect(0, 0, 600, 100);
ctx.beginPath();
ctx.arc(ballX, 50, 20, 0, Math.PI * 2);
ctx.fillStyle = '#6366f1';
ctx.fill();
},
});The same pattern works for Three.js objects, PixiJS sprites, or any imperative renderer. You're just calling a function on every frame. Popmotion handles the timing, the easing math, and the RAF loop. You handle the render.
Another legit pattern: animating CSS custom properties. Instead of animating transform directly, animate a --value property and let CSS do the heavy lifting with calc(). This lets you animate things that would normally require JavaScript recalculation on every frame to instead be handled more efficiently. If you're building glassmorphism components with animated blur or opacity, driving --blur-amount through Popmotion and reading it in CSS is a clean separation.
One more thing — Popmotion works great in Web Components where React isn't present. If you're shipping a component library as native custom elements, Popmotion gives you production-grade animation without pulling in a full framework.
Inertia and Decay: Momentum-Based Animation
Beyond springs and keyframes, Popmotion has two less-discussed animation types: inertia and decay. These are for momentum-based motion — think a swipe gesture that should glide to a stop, or a scroll that decelerates naturally.
import { animate } from 'popmotion';
// Simulate a card being flicked at velocity 800
animate({
from: 0,
velocity: 800,
type: 'inertia',
power: 0.8,
timeConstant: 700,
modifyTarget: (v) => Math.round(v / 100) * 100, // snap to nearest 100px
onUpdate: (v) => {
card.style.transform = `translateX(${v}px)`;
},
});The modifyTarget function is the clever bit. Popmotion first calculates where the element would naturally come to rest based on the initial velocity and decay curve, then passes that predicted final position through your modifyTarget function. You can snap it to a grid, clamp it to bounds, or map it to any value you want. The animation then aims for the modified target using a spring, so you still get that natural settling feel.
This is exactly how swipeable carousels and drag-to-scroll components work under the hood. Framer Motion's drag prop with dragMomentum enabled uses this precise mechanism — the inertia animation in Popmotion is driving it. If you've built anything with react-spring animation, you'll recognize the concept from its useSpring with velocity prop, though the API shape is different.
In practice, decay is simpler than inertia — it just exponentially decelerates a value from an initial velocity with no snapping behavior. Useful for parallax effects or trailing cursor animations where you want smooth deceleration without endpoint snapping.
Popmotion vs the Alternatives in 2026
How does Popmotion stack up against the competition? It's a narrower library than you might expect — it doesn't do scroll-driven animations, it has no DOM binding layer, and it doesn't come with a timeline. For those features you want GSAP or Motion One. But that's also exactly why Popmotion is good at what it does: it's a physics engine, not a full animation framework.
Compared to GSAP with React, Popmotion is free without licensing restrictions and significantly smaller. GSAP's full suite with plugins is over 60KB. Popmotion's animate module is under 6KB. If you only need spring physics and keyframes, paying the GSAP bundle size tax doesn't make sense.
Against Motion One — another lightweight option — Popmotion wins specifically on spring physics accuracy. Motion One uses the Web Animations API under the hood, which doesn't support true spring physics (it only supports duration-based easing). Popmotion's springs are computed per-frame in JavaScript, which means they actually behave like springs. That matters for gesture-driven UIs.
The honest answer is: if you're in a React project, just use Framer Motion. You're already getting Popmotion through it. If you're in a non-React context or need to animate canvas/WebGL, pull in Popmotion directly. Anything more sophisticated — complex timelines, ScrollTrigger pinning, morphing SVGs — GSAP is the right call. You can also browse animation-ready components on Empire UI and modify the motion values to use Popmotion directly if you need custom physics behavior the Framer Motion defaults don't expose.
One more thing — the motion design tokens guide is worth reading alongside this article. Systematizing your spring values (stiffness, damping, mass) as design tokens gives you consistent feel across an entire product instead of hardcoding 400/20/1 everywhere and forgetting why.
FAQ
No — Popmotion is the animation engine that Framer Motion wraps. Framer Motion adds the React API, DOM bindings, gestures, and layout animations on top. You can use Popmotion directly without React at all.
Yes, but you'll be doing manual wiring via refs and effects. In most React projects, using Framer Motion directly is cleaner. Reach for raw Popmotion when you're animating canvas, WebGL, or non-React environments.
Springs animate toward a target value with configurable bounciness. Inertia animates from an initial velocity and gradually decelerates to a rest point, optionally snapping via modifyTarget — useful for swipe and fling gestures.
The core animate function is roughly 5KB gzipped, well under Framer Motion's ~30KB. It's a solid choice when you need physics-based animation without the React overhead.