Mock Framer Motion with Jest

Mock Framer Motion in Jest with React.Forward Reference component animations

Velu S Gautam3 min readLearnings

Overview

Testing components that use framer-motion is often painful because:

  • AnimatePresence and motion.* 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 implement forwardRef, or ref assertions in tests will fail.
  • jest.mock factories are hoisted above imports by babel-jest, so the factory can't reference variables from the outer scope unless they're prefixed with mock — this rules out importing React normally 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 each motion.* primitive with a plain (or forwardRef-wrapped) stand-in, and by pulling in React via an inline require() to sidestep the hoisting restriction.

The Mock

JAVASCRIPT
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

Test file renders component

Component imports from 'framer-motion'

jest.mock('framer-motion') intercepts the module

AnimatePresence → FakeAnimatePresence

motion.* → mocked primitives

Wraps children in FakeTransition

FakeTransition passes children through unchanged

motion.div → forwardRef wrapper, strips 4 known animation props

motion.a / button → jest.fn, spreads all remaining props

motion.li / span / small / h4 / h2 / p → jest.fn, keeps only children

Plain DOM elements render in the test tree

Testing Library / snapshot assertions run against plain HTML

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 targetRef forwardingForwards non-children propsStrips animation propsOutput
motion.aNoYesN/A (nothing stripped)<a {...rest}>
motion.liNoNoN/Abare children
motion.spanNoNoN/Abare children
motion.smallNoNoN/A<small>{children}</small>
motion.h4NoNoN/A<h4>{children}</h4>
motion.h2NoNoN/A<h2>{children}</h2>
motion.pNoNoN/A<p>{children}</p>
motion.divYesYes (minus 4 props)whileTap, animate, initial, variants only<div {...divProps} ref={ref}>
motion.buttonNoYesN/A<button {...rest}>
motion.customNo——returns the raw props object, not a rendered element
AnimatePresenceNoNo (only reads children)N/Achildren wrapped in FakeTransition

Known Limitations

These aren't hypothetical — they follow directly from reading the implementation above:

  1. Silent prop-drop on six elements. li, span, small, h4, h2, and p only destructure children; everything else (className, onClick, data-testid, aria-*, key) is discarded. A test that does getByTestId(...) against one of these will fail even though the component "renders."
  2. Incomplete animation-prop denylist on motion.div. Only whileTap, animate, initial, and variants are 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 trigger Warning: React does not recognize the "X" prop on a DOM element.
  3. No ref forwarding except on div. If a component grabs a ref off motion.span or motion.button in production, that ref will be null in tests.
  4. motion.custom mocked as a component, not a factory. In real framer-motion, motion.custom(Component) is a function that returns a new motion-enabled component. This mock instead treats motion.custom as if it were used directly in JSX and just echoes back its props — it will not behave correctly if your code calls motion.custom(SomeComponent) and renders the result.
  5. AnimatePresence props are ignored. mode, initial, onExitComplete, and custom are 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:

JAVASCRIPT
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.js file at your project root (Jest auto-applies it to every test without needing jest.mock() per file) — this is the better option if more than one test suite needs it.
JAVASCRIPT
// 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();
});

Comments