Best Practices

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?
You can, but it's harder than it sounds. You need to detect when all animations are complete, including those triggered by the wait itself (hover states, focus indicators). Disabling animations is more reliable and makes tests faster.
Will disabling animations hide real bugs?
Potentially. If animations are part of your product's core UX, you might want to test them separately. But for most visual regression testing, static screenshots of the final state are what you care about. Test animations explicitly if they matter.
Does prefers-reduced-motion work for all animations?
Only if your app respects it. Many CSS animations and JavaScript libraries ignore this preference. Using prefers-reduced-motion is a good start, but you'll likely need additional techniques for complete coverage.
How do I disable animations in third-party components?
Global CSS overrides work for CSS-based animations. For JavaScript animations in third-party code, you may need to mock the animation library globally or accept some flakiness. Component libraries often have props to disable animations.
Should I disable animations in development too?
Generally no. Keep animations in development so you experience what users see. Only disable them in your test environment. Use environment variables or test-specific CSS to scope the disabling.

Build visual tests that don't fight your animations

Get early access