ContactSign inSign up
Contact

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:

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 package with the following if you are using a version of Storybook earlier than 8.0:
 * import { within } from "@storybook/testing-library";
 * import { expect } from "@storybook/jest";
*/
import { expect, within } from "@storybook/test";

import { Product } from "./Product";

const meta: Meta<typeof Product> = {
  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 },
  },
};

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

export const Default: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await expect(canvas.getByText("Product details")).toBeInTheDocument();
  },
};
ℹ️ The chromatic.pauseAnimationAtEnd parameter can be set at story, component, and project levels. This enables you to set project wide defaults and override them for specific components and/or stories. Learn more »

JavaScript animations

If you’re working with JavaScript animations libraries (e.g., framer-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() 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:

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

MotionGlobalConfig.skipAnimations = isChromatic();

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

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.

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();
  });
});

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

// @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 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.

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.

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.

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 package with the following if you are using a version of Storybook earlier than 8.0:
    * import { within } from "@storybook/testing-library";
    * import { expect } from "@storybook/jest";
  */
  import { expect, within, waitFor } from "@storybook/test";

  import { Button } from "./Button";

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

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

  export const Default: Story = {
    play: async ({ canvasElement }) => {
      const canvas = within(canvasElement);
      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 },
      );
    },
  };

✨ Here’s a live demo that you can experiment with.

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.

MyComponent.stories.tsx
import { screen } 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 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 to allow the animation to complete before taking the snapshot.

Alternatively, ignore an element to omit a visible area of your component when comparing snapshots.

Troubleshooting

Why are my animations working differently in Chromatic?

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 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.