---
title: Animations
description: Learn how Chromatic pauses animations and how to control the behavior
sidebar: { order: 10 }
---

import ParamsCallout from "../../components/ParamsCallout.astro";
import { YouTubeCallout } from "../../components/YouTubeCallout";
import IntegrationSnippets from "../../components/IntegrationSnippets.astro";

# Animations

Chromatic proactively pauses CSS animations/transitions, SVG animations, and videos to prevent false positives. We do this because multiple variables outside of our control make it impossible to guarantee consistent animation painting down to the millisecond.

## CSS animations

By default, CSS animations are paused at the end of their animation cycle (i.e., the last frame) when your tests run in Chromatic. This behavior is useful, specifically when working with animations that are used to "animate in" visible elements. If you want to override this behavior and pause the animation at the first frame, add the `pauseAnimationAtEnd` configuration option to your tests. For example:

{/* prettier-ignore-start */}

<IntegrationSnippets>
  <Fragment slot="storybook">
    ```ts title="src/components/Product.stories.ts|tsx"
    // Adjust this import to match your framework (e.g., nextjs, vue3-vite)
    import type { Meta, StoryObj } from "@storybook/your-framework";

    /*
     * Replace the storybook/test import with `@storybook/test` and adjust the stories accordingly if you're not using Storybook 9.0.
     * Refer to the Storybook documentation for the correct package and imports for earlier versions.
    */
    import { expect } from "storybook/test";

    import { Product } from "./Product";

    const meta = {
      component: Product,
      title: "Product",
      parameters: {
        // Overrides the default behavior and pauses the animation at the first frame at the component level for all stories.
        chromatic: { pauseAnimationAtEnd: false },
      },
    } satisfies Meta<typeof Product>;

    export default meta;
    type Story = StoryObj<typeof meta>;

    export const Default: Story = {
      play: async ({ canvas }) => {
        await expect(canvas.getByText("Product details")).toBeInTheDocument();
      },
    };
    ```
  <ParamsCallout name="pauseAnimationAtEnd" integration="storybook" />
  </Fragment>
  <Fragment slot="playwright">
    ```ts title="tests/Product.spec.js|ts"
    import { test, expect } from "@chromatic-com/playwright";

    test.describe("Products Page", () => {
      // Overrides the default behavior and pauses the animation at the first frame for this test.
      test.use({ pauseAnimationAtEnd: false });

      test("Successfully loads the page", async ({ page }) => {
        await page.goto("/products");
        await expect(page.getByText("Product details")).toBeVisible();
      });
    });
    ```
  <ParamsCallout name="pauseAnimationAtEnd" integration="playwright" />
  </Fragment>
  <Fragment slot="cypress">
    ```ts title="cypress/e2e/Product.cy.js|ts"
    describe("Not found", () => {
      it("Successfully loads the page", { env: {
        pauseAnimationAtEnd: false // Overrides the default behavior and pauses the animation at the first frame for this test.
      }}, () => {
        cy.visit("/products");
        cy.get("h3").should("contain", "Product details");
      });
    });
    ```
  <ParamsCallout name="pauseAnimationAtEnd" integration="cypress" />
  </Fragment>
</IntegrationSnippets>

{/* prettier-ignore-end */}

## JavaScript animations

If you're working with JavaScript animations libraries (e.g., [framer-motion](https://www.framer.com/motion/)), Chromatic will not have the same level of control over the animations as CSS animations and will not disable them by default. We recommend toggling off the animation library when running in Chromatic to ensure consistent visual tests and avoid false positives.

### Storybook

You can conditionally disable animations using the [`isChromatic()`](/docs/ischromatic) utility function. For example, to turn off animations globally in framer-motion (v10.17.0 and above), you can set the `MotionGlobalConfig.skipAnimations` option as follows:

```ts title=".storybook/preview.js|ts"
import { MotionGlobalConfig } from "framer-motion";
import isChromatic from "chromatic/isChromatic";

MotionGlobalConfig.skipAnimations = isChromatic();
```

<div class="aside">

ℹ️ For more information on disabling animations in other libraries, refer to the library's documentation or community resources to learn how to achieve this.

</div>

You can also replace the animation with a static image for visual testing. Watch the video below to see how this technique works.

<YouTubeCallout
  client:load
  id="SyQfmoU2Ato"
  summary="Replace animation with a static image for visual testing"
/>

### Playwright and Cypress

When using Playwright or Cypress, you can assert that specific elements are visible in the DOM to confirm an animation has finished. Alternatively, use the `wait` or `waitForTimeout` functions to wait for the animation to complete.

Another strategy is to inject a variable into the window object to disable animations. For example, in your E2E test, set the `disableAnimations` property on the `window` object to `true`.

{/* prettier-ignore-start */}

<IntegrationSnippets>
  <Fragment slot="playwright">
    ```ts title="tests/Product.spec.js|ts"
    import { test, expect } from "@chromatic-com/playwright";

    test.describe("Products Page", () => {
      test("Successfully loads the page", async ({ page }) => {
        // Set a property on the the window object to disable animations.
        await page.addInitScript(() => {
          window.disableAnimations = true
        });

        await page.goto("/products");
        await expect(page.getByText("Product details")).toBeVisible();
      });
    });
    ```
  </Fragment>
  <Fragment slot="cypress">
    ```ts title="cypress/e2e/Product.cy.js|ts"
    describe("Not found", () => {
        // Set a property on the the window object to disable animations.
        Cypress.on('window:before:load', (win) => {
          win.disableAnimations = true
        })

      it("Successfully loads the page", () => {
        cy.visit("/products");
        cy.get("h3").should("contain", "Product details");
      });
    });
    ```
  </Fragment>
</IntegrationSnippets>

{/* prettier-ignore-end */}

Then read the value of the `disableAnimations` property in your application code to conditionally disable animations.

```js
// @ts-ignore
if (window.disableAnimations) {
  MotionGlobalConfig.skipAnimations = true;
}
```

### Use a play function to wait for animations to complete

If your animation library doesn't support disabling animations, you can use a [play function](https://storybook.js.org/docs/writing-stories/play-function) to assert that animation is complete before taking a snapshot.

To accomplish this, add a property (ID, class or data attribute) to the element you’re testing once the animation finishes. Then, in the `play()` function, check for this property's presence. This ensures that the UI is in a stable state and ready for Chromatic to capture a snapshot.

Here's a quick example that demonstrates this approach using a React hook that tracks the state of your animation and adds a `testid` data attribute on completion.

```tsx title="useAnimatedState.tsx"
export const useAnimatedState = () => {
  const [isComplete, setIsComplete] = useState(false);

  const handleAnimationComplete = useCallback(() => {
    setIsComplete(true);
  }, [setIsComplete]);

  return {
    isComplete,
    animationProps: {
      onAnimationComplete: handleAnimationComplete,
      "data-animation": isComplete ? "complete" : "playing",
      "data-testid": "animated-element",
    },
  };
};
```

Then use the hook within the component that contains animated elements.

```tsx title="Button.tsx"
import { motion } from "framer-motion";
import { useAnimatedState } from "./useAnimatedState";

export const Button = () => {
  const { animationProps } = useAnimatedState();

  return (
    <motion.button
      initial={{ scale: 0 }}
      animate={{ scale: 2 }}
      transition={{ duration: 4 }}
      // the animation props will add the necessary data attributes when animation is completed
      {...animationProps}
    >
      Animated element...
    </motion.button>
  );
};
```

Lastly, add a `play()` function to the story file that asserts that the animation is complete.

```tsx title="MyComponent.stories.tsx"
// Adjust this import to match your framework (e.g., nextjs, vue3-vite)
import type { Meta, StoryObj } from "@storybook/your-framework";

/*
 * Replace the storybook/test import with `@storybook/test` and adjust the stories accordingly if you're not using Storybook 9.0.
 * Refer to the Storybook documentation for the correct package and imports for earlier versions.
 */
import { expect, waitFor } from "storybook/test";

import { Button } from "./Button";

const meta = {
  component: Button,
  title: "Button",
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
  play: async ({ canvas }) => {
    await waitFor(
      async () => {
        await expect(canvas.getByTestId("animated-element")).toHaveAttribute(
          "data-animation",
          "complete",
        );
      },
      // Default timeout is 1s, but you
      // can pass options if it take longer
      { timeout: 5000 },
    );
  },
};
```

<div class="aside">

✨ Here's a [live demo](https://stackblitz.com/edit/github-y4kx5ttm?file=src%2Fstories%2FButton.tsx,src%2Fstories%2FuseAnimatedState.ts,src%2Fstories%2FButton.stories.ts&preset=node) that you can experiment with.

</div>

Additionally, you can turn this into a utility in order to reuse in other stories. If you happen to have multiple animated elements, there's some adjustments you can do to make this work. Here's an example below that both incorporates multiple animated elements and creates a utility for reuse in other stories.

```tsx title="MyComponent.stories.tsx"
/*
 * Replace the storybook/test import with `@storybook/test` and adjust the stories accordingly if you're not using Storybook 9.0.
 * Refer to the Storybook documentation for the correct package and imports for earlier versions.
 */
import { screen, waitFor } from "storybook/test";

const waitForAllAnimationsToComplete = async () => {
  return waitFor(
    () => {
      // Find all elements with animated-element testid added by the hook
      const elements = screen.getByTestId("animated-element");

      // Make sure they all finish their animation
      elements.forEach((element) => {
        expect(element).toHaveAttribute("data-animation", "complete");
      });
    },
    // Default timeout is 1s, but you
    // can pass options if it takes longer
    { timeout: 5000 },
  );
};

export const MultipleAnimationExample: Story = {
  play: async () => {
    // 👇 Call the utility in your play() function
    await waitForAllAnimationsToComplete();
  },
};
```

## GIFs and Videos

Chromatic automatically pauses videos and animated GIFs at their first frame, ensuring consistent visual tests without the need for custom workarounds. If you specify a [poster](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#poster) attribute for videos, Chromatic will use that image instead.

## Animations that cannot be disabled

If you cannot turn off animations (for example, if disabling JavaScript animations is complex or your library doesn't support it), you can use a [delay](/docs/delay) to allow the animation to complete before taking the snapshot.

Alternatively, [ignore an element](/docs/ignoring-elements) to omit a visible area of your component when comparing snapshots.

## Troubleshooting

<details>
<summary>Why are my animations working differently in Chromatic?</summary>

If you're experiencing issues with animations not being paused as expected, it is likely due to an infrastructure update. With Capture Stack's [version 6](/docs/infrastructure-release-notes#version-6) general availability (released in February 2024), the `pauseAnimationAtEnd` feature was enabled by default, leading to a change in behavior. If your tests relied on the previous behavior, you need to update your tests and configure the `pauseAnimationAtEnd` option to `false`.

</details>