Mock Framer Motion with Jest
Mock Framer Motion in Jest with React.Forward Reference component animations
Overview
Testing components that use framer-motion is often painful because:
AnimatePresenceandmotion.*components carry animation-specific props (whileTap,initial,variants, etc.) that aren't valid HTML attributes — if they reach a real DOM node, React logs "unknown prop" warnings.- Components that forward refs through
motion.div(a common pattern for imperative focus/measurement logic) need the mock to actually implementforwardRef, orrefassertions in tests will fail. jest.mockfactories are hoisted above imports bybabel-jest, so the factory can't reference variables from the outer scope unless they're prefixed withmock— this rules out importingReactnormally at the top of the test file and using it inside the factory. The mock below solves all three problems for the elements it covers, by replacing eachmotion.*primitive with a plain (orforwardRef-wrapped) stand-in, and by pulling inReactvia an inlinerequire()to sidestep the hoisting restriction.
The Mock
jest.mock('framer-motion', () => {
const FakeTransition = jest
.fn()
.mockImplementation(({ children }) => children);
const FakeAnimatePresence = jest
.fn()
.mockImplementation(({ children }) => (
<FakeTransition>{children}</FakeTransition>
));
const motion = {
a: jest
.fn()
.mockImplementation(({ children, ...rest }) => (
<a {...rest}>{children}</a>
)),
li: jest.fn().mockImplementation(({ children }) => children),
span: jest.fn().mockImplementation(({ children }) => children),
small: jest
.fn()
.mockImplementation(({ children }) => <small>{children}</small>),
h4: jest.fn().mockImplementation(({ children }) => <h4>{children}</h4>),
h2: jest.fn().mockImplementation(({ children }) => <h2>{children}</h2>),
p: jest.fn().mockImplementation(({ children }) => <p>{children}</p>),
div: require('react').forwardRef(({ children, ...rest }, ref) => {
const { whileTap, animate, initial, variants, ...divProps } = rest;
return (
<div {...divProps} ref={ref}>
{children}
</div>
);
}),
button: jest
.fn()
.mockImplementation(({ children, ...rest }) => (
<button {...rest}>{children}</button>
)),
custom: jest.fn().mockImplementation((props) => props),
};
return {
__esModule: true,
motion: motion,
AnimatePresence: FakeAnimatePresence,
default: jest.fn(),
};
});How It Works
At a high level: Jest's module registry substitutes the real framer-motion export with this object before the component under test ever imports it. Every motion.X and AnimatePresence reference in the component resolves to a lightweight stand-in that renders ordinary DOM elements, so assertions and queries (getByRole, getByText, snapshots, etc.) behave as if animation was never involved.
Element-by-Element Reference
| Mock target | Ref forwarding | Forwards non-children props | Strips animation props | Output |
|---|---|---|---|---|
motion.a | No | Yes | N/A (nothing stripped) | <a {...rest}> |
motion.li | No | No | N/A | bare children |
motion.span | No | No | N/A | bare children |
motion.small | No | No | N/A | <small>{children}</small> |
motion.h4 | No | No | N/A | <h4>{children}</h4> |
motion.h2 | No | No | N/A | <h2>{children}</h2> |
motion.p | No | No | N/A | <p>{children}</p> |
motion.div | Yes | Yes (minus 4 props) | whileTap, animate, initial, variants only | <div {...divProps} ref={ref}> |
motion.button | No | Yes | N/A | <button {...rest}> |
motion.custom | No | — | — | returns the raw props object, not a rendered element |
AnimatePresence | No | No (only reads children) | N/A | children wrapped in FakeTransition |
Known Limitations
These aren't hypothetical — they follow directly from reading the implementation above:
- Silent prop-drop on six elements.
li,span,small,h4,h2, andponly destructurechildren; everything else (className,onClick,data-testid,aria-*,key) is discarded. A test that doesgetByTestId(...)against one of these will fail even though the component "renders." - Incomplete animation-prop denylist on
motion.div. OnlywhileTap,animate,initial, andvariantsare stripped. Any other framer-motion prop your components use —whileHover,whileFocus,whileInView,exit,transition,layout,layoutId,drag,dragConstraints,onAnimationStart,onAnimationComplete— will land on the real<div>and triggerWarning: React does not recognize the "X" prop on a DOM element. - No ref forwarding except on
div. If a component grabs a ref offmotion.spanormotion.buttonin production, that ref will benullin tests. motion.custommocked as a component, not a factory. In realframer-motion,motion.custom(Component)is a function that returns a new motion-enabled component. This mock instead treatsmotion.customas if it were used directly in JSX and just echoes back its props — it will not behave correctly if your code callsmotion.custom(SomeComponent)and renders the result.AnimatePresenceprops are ignored.mode,initial,onExitComplete, andcustomare all dropped; tests that assert on exit-animation callbacks won't have anything to observe.
Suggested Improvement (Optional)
If the dropped-prop issue (#1) bites you later, a single generic factory removes the inconsistency without adding much code:
const passthrough = (Tag) =>
jest.fn().mockImplementation(({ children, ...rest }) => (
<Tag {...rest}>{children}</Tag>
));
const motion = {
a: passthrough('a'),
li: passthrough('li'),
span: passthrough('span'),
small: passthrough('small'),
h4: passthrough('h4'),
h2: passthrough('h2'),
p: passthrough('p'),
button: passthrough('button'),
div: require('react').forwardRef(({ children, ...rest }, ref) => {
const {
whileTap, whileHover, whileFocus, whileInView, drag, dragConstraints,
animate, initial, exit, variants, transition, layout, layoutId,
...divProps
} = rest;
return (
<div {...divProps} ref={ref}>
{children}
</div>
);
}),
custom: jest.fn().mockImplementation((props) => props),
};This keeps every element's behavior consistent (props spread, children preserved) and widens the animation-prop denylist to cover the most commonly used framer-motion props.
Usage
Place the jest.mock('framer-motion', ...) call either:
- Inline at the top of an individual test file, above your component import, or
- In a shared
__mocks__/framer-motion.jsfile at your project root (Jest auto-applies it to every test without needingjest.mock()per file) — this is the better option if more than one test suite needs it.
// Component.test.jsx
jest.mock('framer-motion', () => { /* mock from above */ });
import { render, screen } from '@testing-library/react';
import Component from './Component';
test('renders without animation-related warnings', () => {
render(<Component />);
expect(screen.getByRole('button')).toBeInTheDocument();
});