How to Reduce Chromatic Snapshot Changes Caused by Animations
Make Chromatic snapshots consistent by choosing the right animation state, disabling JavaScript motion, and waiting for explicit completion.
To reduce Chromatic snapshot changes caused by animations, make each visual test capture the same intentional UI state. Chromatic already pauses CSS transitions and CSS/SVG animations; CSS animations default to their final frame. Set chromatic.pauseAnimationAtEnd: false when you want the first frame. For JavaScript-driven motion, disable the animation in visual-test runs or wait for an explicit completion condition before capture.
Start by identifying what drives the motion, deciding which frame or interaction state matters, and applying the narrowest reliable control. A fixed delay can help when no condition is available, but it is less reliable than checking that the intended state is ready.
1. Identify the animation and intended capture state
Different motion mechanisms need different remedies. Before changing a setting, answer two questions: what is animating, and what should the snapshot show?
| Mechanism | Chromatic behavior | Useful approach |
|---|---|---|
| CSS transition or CSS/SVG animation | Chromatic pauses it. CSS animations pause at the end of their cycle by default. | Keep the default for a final state, or set pauseAnimationAtEnd: false for the first frame. |
| JavaScript animation library | Chromatic does not disable JavaScript-driven motion automatically. | Use the library’s supported test setting, or gate capture on an explicit completed state. |
| Animated GIF or video | Chromatic pauses animated GIFs and videos at the first frame. If a video has a poster, Chromatic uses it. | Use a stable first frame or poster appropriate to the story. |
| Interaction-driven movement | The screenshot may be taken before the interaction reaches its intended result. | Complete the interaction and assert the resulting state in the Storybook play function or browser test. |
Do not disable motion indiscriminately if the animation itself is what the story is meant to test. In that case, make the tested point in the animation reproducible and capture it deliberately.
2. Choose the CSS animation frame
Chromatic’s CSS animation setting is chromatic.pauseAnimationAtEnd. By default, CSS animations pause at their cycle’s end. Set it to false to capture their first frame. Storybook parameters can be set at story, component, or project level; choose the narrowest scope that fits.
Set the first frame for one story
import type { Meta, StoryObj } from '@storybook/react';
import { AnimatedNotice } from './AnimatedNotice';
const meta = {
component: AnimatedNotice,
} satisfies Meta<typeof AnimatedNotice>;
export default meta;
type Story = StoryObj<typeof meta>;
export const EntranceAtStart: Story = {
parameters: {
chromatic: {
pauseAnimationAtEnd: false,
},
},
};
This is a complete Storybook story module assuming the component exists at the stated import path. Use the same parameter on a component’s default export or in project-level Storybook parameters only when the same frame choice is correct for every affected story.
Keep the final frame
If the final visual state is the reference you want, leave the setting at its default. Avoid adding a delay just to let a CSS animation finish: Chromatic already pauses CSS animation at the end of its cycle.
Chromatic says the default changed with Capture Stack version 6 general availability in February 2024. If an older project appears to capture a different frame, check the project’s capture-stack history and set the parameter explicitly when you need consistent behavior across configurations.
3. Handle JavaScript animation libraries
Chromatic cannot disable JavaScript-driven animations automatically. Prefer a library-supported switch in visual-test runs. For Framer Motion 10.17.0 and later, Chromatic documents using isChromatic() to set MotionGlobalConfig.skipAnimations.
import { isChromatic } from 'chromatic';
import { MotionGlobalConfig } from 'framer-motion';
if (isChromatic()) {
MotionGlobalConfig.skipAnimations = true;
}
Place this in Storybook’s preview setup so it runs in the visual-test environment before stories render. Confirm that the selected library version supports the API; other animation libraries need their own documented mechanism. If motion is part of the behavior under test, do not skip it. Instead, synchronize capture with the intended completed state.
Use a test-only flag when the browser test owns the page
For Playwright or Cypress, pass a test-only flag into the page and let application code read it to disable motion. The exact way to pass the flag depends on the test setup; the application must apply it before the animated component starts. Keep the branch limited to test runs so ordinary users retain the intended animation.
4. Wait for the intended state instead of guessing
Chromatic uses network quiescence in part as a signal that resources have loaded. Network inactivity does not prove that a JavaScript animation has finished. Establish readiness with a DOM condition, completion marker, or assertion tied to the state the reference represents.
Storybook interaction tests
Chromatic waits for a Storybook play function to finish before taking that interaction-test snapshot. Complete the interaction and assert the resulting state there:
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { Menu } from './Menu';
const meta = { component: Menu } satisfies Meta<typeof Menu>;
export default meta;
type Story = StoryObj<typeof meta>;
export const OpensAfterClick: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button', { name: 'Open menu' }));
await expect(canvas.getByRole('menu')).toBeVisible();
},
};
Replace the component and accessible names with those in your story. The assertion should represent the state you want captured. If the menu animates after becoming visible, visibility alone may not prove the motion has completed; use an application-level completion signal or a suitable animation-state assertion when available.
Playwright and Cypress
Use the integration’s documented browser assertions to check visibility or wait for the relevant animation state before a targeted snapshot. Prefer a condition that describes the UI state over a fixed duration. Chromatic’s Playwright configuration guidance covers integration-specific capture controls; consult it when adapting a browser test.
Use a delay only when a state condition is not practical
Storybook supports chromatic.delay to wait before capture. Choose a delay based on the animation’s actual behavior and keep it scoped to the affected story or component. A delay can be too short when the animation or page is slow, and unnecessarily long on every run.
5. Use capture controls for their intended purpose
| Control | What it changes | Use it when |
|---|---|---|
chromatic.pauseAnimationAtEnd |
Chooses whether CSS animation pauses at its end or at its first frame. | The CSS animation is meaningful but the reference needs a consistent frame. |
chromatic.delay |
Waits before capture. | A bounded wait is required and a readiness condition is unavailable. |
chromatic.prefersReducedMotion |
Sets the reduced-motion media preference for the capture. | The story should render its reduced-motion design variant. |
chromatic.ignoreSelectors |
Excludes matching regions from visual comparison. | A region is outside the purpose of the snapshot and cannot be made stable another way. |
chromatic.disableSnapshot |
Disables the Storybook snapshot. | The story should not create a snapshot, rather than needing a deterministic one. |
These settings serve different goals. Reduced motion tests the UI’s response to a user preference; it does not mean every animation library is disabled. Ignoring a selector removes that region from comparison, so do not use it for the behavior the story is supposed to verify. Disabling a snapshot suppresses capture rather than fixing animation nondeterminism.
6. Disable automatic snapshots only for targeted capture workflows
Chromatic documents disableAutoSnapshot for Vitest, Playwright, and Cypress when you want to take targeted snapshots instead of the default end-of-test snapshot. This is distinct from making the automatic snapshot stable: it changes when or whether that default snapshot is taken. See Chromatic’s guidance on disabling snapshots before adopting that workflow.
7. Troubleshoot changing snapshots
| Symptom | Likely cause | Fix |
|---|---|---|
| The CSS entrance animation shows its last frame. | The default is to pause CSS animation at the end of its cycle. | Set chromatic.pauseAnimationAtEnd: false on the affected story or component to capture the first frame. |
| A JavaScript animation is at a different point on each run. | Network quiescence does not guarantee JavaScript motion has completed, and Chromatic does not disable it automatically. | Disable motion through the library’s supported test setting, or assert a completed state before capture. |
| A delay fixes some runs but not others. | The chosen duration is not a reliable readiness condition for variable animation or page timing. | Wait for a DOM condition or completion marker; if that is unavailable, tune and scope the delay to the actual animation. |
| A video or GIF does not show its animated point. | Chromatic pauses animated media at the first frame; a video poster may be used instead. | Provide a suitable stable first frame or video poster for the snapshot. |
| The reduced-motion variant did not disable a library animation. | The media preference and a library’s JavaScript animation controls are separate mechanisms. | Use the library’s test-specific setting, and use prefersReducedMotion when testing the reduced-motion experience itself. |
| The animation region disappeared from comparisons. | An ignore selector excludes content, or snapshot capture was disabled. | Review ignoreSelectors and snapshot-disable settings; remove them if that visual region should be tested. |
| A setting works on one story but affects too many or too few. | The parameter is set at the wrong scope. | Move it to the story, component, or project level that matches the stories sharing the intended frame behavior. |
8. Performance, reliability, and maintenance
A condition-based wait usually avoids paying the same fixed delay on every run while still waiting for slow cases. Keep readiness checks specific: a page-wide network-idle condition can indicate that resources have settled, but it does not establish that a JavaScript animation reached the desired frame.
Prefer explicit animation settings where the frame matters, and scope them narrowly. Project-wide changes can alter many references at once. If a library or Chromatic capture behavior changes, recheck the chosen mechanism and expected state. When motion is intentionally under test, keep the animation visible in the comparison and synchronize the snapshot to a meaningful point.
This workflow has no screenshot API usage cost: it configures Chromatic and the visual-test code already used by the project. If you choose to use a separate screenshot API for other capture jobs, compare its current documented behavior and pricing before adopting it.
Or skip the browser setup
For standalone website captures outside Chromatic’s visual-test workflow, ScreenshotNeo is a website screenshot API and MCP server. A single GET request returns a PNG, JPEG, WebP, or PDF. Its cookie and consent handling accepts banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status in headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents.
It has 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan. See the ScreenshotNeo API documentation for options and configuration.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Does Chromatic capture CSS animations at the same point every time?
It pauses CSS animations deterministically at the end by default. Choose the first frame with pauseAnimationAtEnd: false when that is the intended reference.
Does turning on reduced motion replace animation-library configuration?
No. Reduced motion sets a media preference; JavaScript animation libraries may require their own test setting.
Should I ignore an animated element?
Only if that region is irrelevant to the snapshot’s purpose. Otherwise, disabling or synchronizing the animation preserves its visual value in the test.
Why can network idle still capture an animation mid-motion?
Network inactivity signals resource loading has settled, not that JavaScript animation has completed. Wait for the intended UI state explicitly.


