Guides

Screenshot Stabilization

Make your visual tests more stable and reliable

Visual tests can be flaky due to dynamic content, animations, and elements that change between test runs. This guide shows you how to stabilize your screenshots for more reliable testing.

Why Stabilization Matters

Screenshots can fail due to:

  • Animations - Elements still moving when screenshots are taken
  • Dynamic content - Timestamps, user data, random content
  • Loading states - Spinners, placeholders that appear inconsistently
  • Third-party content - Ads, widgets that load at different times

Stabilization techniques help create consistent, reliable visual tests.

CSS Injection

CSS injection lets you modify how your UI appears during screenshot capture by injecting custom CSS globally.

Disabling Animations

Prevent flaky tests from animations:

// visnap.config.ts
adapters: {
  browser: {
    name: "@visnap/playwright-adapter",
    options: {
      injectCSS: `
        * { 
          animation: none !important; 
          transition: none !important; 
        }
      `
    }
  }
}

Hiding Dynamic Content

Hide elements that change between test runs:

injectCSS: `
  .timestamp { display: none !important; }
  .user-avatar { background: #ccc !important; }
  .loading-spinner { opacity: 0 !important; }
  .ad-slot { visibility: hidden !important; }
`

Per-Test Override

Individual tests can disable CSS injection:

// In your story or URL config
{
  disableCSSInjection: true
}

Element Masking

Element masking overlays specific elements with solid colors before taking screenshots, hiding dynamic content without affecting the rest of your UI.

When to Use Masking

Mask elements that:

  • Change between test runs (timestamps, user data)
  • Load at different times (ads, third-party widgets)
  • Contain random content (recommendations, "related items")
  • Show loading states inconsistently

Configuration

Storybook:

export const MyStory: Story = {
  parameters: {
    visualTesting: {
      elementsToMask: [".timestamp", "#ad-slot", ".user-avatar"]
    }
  }
};

URL Adapter:

{
  id: "homepage",
  url: "http://localhost:3000/",
  elementsToMask: [".sticky-header", "#ad-banner", ".timestamp"]
}

Selector Tips

Use specific selectors to avoid masking unintended elements:

// Good: Specific selectors
elementsToMask: [
  ".timestamp",           // All timestamp elements
  "#ad-slot",            // Specific ad container
  ".user-avatar img"     // Avatar images only
]

// Avoid: Too broad
elementsToMask: [
  "div",                 // Would mask all divs
  ".content"             // Might mask important content
]

Best Practices

Choose the Right Technique

CSS Injection works best for:

  • Disabling animations globally across all tests
  • Hiding many different types of elements at once
  • Modifying element styling (colors, visibility)

Element Masking works best for:

  • Hiding specific dynamic elements per test
  • Per-test control over what's hidden
  • Preserving layout while hiding content

Combine Techniques

You can use both techniques together:

// Global CSS injection for animations
injectCSS: `* { animation: none !important; }`

// Per-test masking for specific elements
elementsToMask: [".timestamp", "#ad-slot"]

Test Your Stabilization

After adding stabilization, run your tests multiple times to ensure they're stable:

npx visnap test
npx visnap test
npx visnap test

Common Patterns

E-commerce sites:

elementsToMask: [
  ".price",              // Dynamic pricing
  ".inventory-count",    // Stock levels
  ".user-reviews"        // User-generated content
]

Dashboards:

elementsToMask: [
  ".last-updated",       // Timestamps
  ".user-profile",       // User-specific data
  ".chart-legend"        // Dynamic chart data
]

Marketing pages:

injectCSS: `
  .countdown-timer { display: none !important; }
  .social-proof { opacity: 0 !important; }
`

Troubleshooting

CSS Not Applied

If your CSS injection isn't working:

  • Check for syntax errors in your CSS
  • Verify the selectors are correct
  • Make sure disableCSSInjection isn't set to true

Elements Not Masked

If element masking isn't working:

  • Verify selectors exist on the page
  • Check for typos in CSS selectors
  • Ensure elements are visible when the page loads

Still Getting Flaky Tests

If tests are still unstable:

  • Add more specific selectors
  • Increase wait times in interactions
  • Consider if the content is truly dynamic or just slow to load

Next Steps