Disable animations for stable visual tests
Animations are the most common source of visual test flakiness. Learn how to disable CSS transitions, JavaScript motion, and async effects for deterministic screenshots.
Why animations cause false positives
A visual test captures a single frame. If any element is mid-animation when the screenshot fires, you'll get inconsistent results across runs.
Frame timing variance
Screenshots capture a single frame. If an animation is mid-transition, you'll capture different frames on different runs.
Duration inconsistency
Animation timing isn't perfectly deterministic. A 300ms transition might complete at 298ms or 305ms depending on system load.
Easing differences
Easing functions produce different visual states throughout the animation. Capturing mid-ease creates unpredictable results.
Lazy loading triggers
Intersection observers and scroll-triggered animations fire at different times depending on rendering speed.
The core problem: screenshots are instantaneous, but animations are temporal. To get reliable visual tests, you need to remove the temporal element.
Approaches to disabling animations
There's no single technique that catches everything. Most teams combine multiple approaches.
Global CSS disable
Inject CSS that sets transition-duration and animation-duration to 0s. Catches most CSS-based motion.
prefers-reduced-motion
Emulate the reduced motion media query. Apps respecting this preference will disable animations automatically.
JavaScript animation control
Mock or disable animation libraries (Framer Motion, GSAP, etc.) at the framework level.
Wait for stability
Wait for animations to complete rather than disabling them. More realistic but harder to implement reliably.
Global CSS disable
The most reliable approach for CSS animations. Inject a stylesheet that forces all transitions and animations to complete instantly:
/* Add to page before screenshot */
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}In Playwright, inject this CSS using page.addStyleTag():
await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`,
});prefers-reduced-motion strategy
Modern browsers support the prefers-reduced-motion media query. Well-built apps disable animations when this preference is set.
In Playwright, emulate this preference:
await page.emulateMedia({ reducedMotion: 'reduce' });This only works if your app respects the preference. Check your CSS for @media (prefers-reduced-motion: reduce) rules. If your app doesn't have them, this technique won't help.
Disabling JavaScript animations
CSS techniques don't catch JavaScript-driven animations. For libraries like Framer Motion, you need to disable them at the library level.
For Framer Motion, use the MotionConfig provider:
import { MotionConfig } from 'framer-motion';
// Wrap your app in tests
<MotionConfig reducedMotion="always">
<App />
</MotionConfig>For other libraries, check their documentation for test mode or instant mode options. If none exist, you may need to mock the library entirely or stub its timing functions.
Waiting for stability vs disabling
An alternative to disabling animations is waiting for them to complete. This is more realistic but harder to implement reliably.
Playwright's toHaveScreenshot() has a built-in stability check—it waits for the page to stop changing before capturing. But this has limits:
- It can't detect animations that loop infinitely
- It adds time to every screenshot (waiting for stability timeout)
- Subtle animations may fall below the detection threshold
For most teams, disabling animations is more reliable than waiting. Use waiting only when you specifically need to test animation end states.
Before you capture screenshots
- Inject CSS to disable transitions and animations globally
- Emulate prefers-reduced-motion in your test browser
- Mock JavaScript animation libraries (Framer Motion, GSAP, etc.)
- Disable loading spinners and skeleton screens
- Wait for any remaining async operations before capture
- Test that your disable technique actually works (capture twice, compare)
- Keep animation-disable code scoped to test environment only
Related guides
Frequently Asked Questions
Why can't I just wait for animations to finish?
Will disabling animations hide real bugs?
Does prefers-reduced-motion work for all animations?
How do I disable animations in third-party components?
Should I disable animations in development too?
Build visual tests that don't fight your animations
Get early access