Blog Testing 8 min read

Percy and Chromatic: Placeholder Images in UI Tests

Use chromatic placeholder images and Percy fixtures to produce stable visual regression baselines by replacing real CDN images with deterministic fallback URLs.

Chromatic placeholder imagesPercy visual testingStorybook snapshotsVisual regressionUI testing
Percy and Chromatic: Placeholder Images in UI Tests

Percy and Chromatic are visual regression services built around Storybook and component snapshots. Both compare screenshots pixel-by-pixel and flag any difference as a change requiring human review. Real remote images in story args or test fixtures introduce noise: the same image can render with different compression or dimensions across CI runs, triggering false positive change requests.

Replacing remote image props with chromatic placeholder images from fallback.pics gives your visual test suite a stable, deterministic baseline that only changes when your component UI changes.

Problem

Why remote images in Storybook args produce unstable snapshots

A story that renders a product card with src pointing to a real product image on Cloudinary or AWS S3 is only stable as long as that image never changes. CDN purge, re-upload, or format renegotiation can alter the pixels that Percy or Chromatic capture, triggering a change that has nothing to do with your component.

Chromatic's TurboSnap can mark stories as unaffected based on git diff, but if the image URL is in a fixture file that changes independently of the component, the story rerenders with a different image and generates a diff.

Deterministic placeholder URLs from fallback.pics return identical pixels for the same URL on every request. No CDN purge, no re-upload, no format change. The only reason a Chromatic snapshot changes is if your story or component changed.

Storybook

Setting placeholder image args in Storybook stories

Replace real image URLs in story args with fallback.pics URLs. Name the stories to reflect what real content they represent (not what the placeholder looks like) so the story library remains meaningful.

Use the same fallback.pics URL across all story variants that share a layout. Different colors or text labels help distinguish content types visually.

Implementation tsx
// src/components/ProductCard/ProductCard.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ProductCard } from './ProductCard';

const meta: Meta<typeof ProductCard> = {
  title: 'Ecommerce/ProductCard',
  component: ProductCard,
};
export default meta;

type Story = StoryObj<typeof ProductCard>;

export const Default: Story = {
  args: {
    name: 'Sample Product',
    price: 29.99,
    // Deterministic placeholder — stable across all CI runs
    imageUrl: 'https://fallback.pics/api/v1/400x400/7C3AED/FFFFFF?text=Product',
  },
};

export const WithLongTitle: Story = {
  args: {
    name: 'A Very Long Product Name That Wraps to Multiple Lines',
    price: 99.99,
    imageUrl: 'https://fallback.pics/api/v1/400x400/3B82F6/FFFFFF?text=Product',
  },
};

export const Loading: Story = {
  args: {
    name: '',
    price: 0,
    imageUrl: 'https://fallback.pics/api/v1/animated/skeleton/400x400',
  },
};

Percy

Percy snapshots with mocked image requests

Percy captures screenshots during Cypress, Playwright, or Storybook test runs. When using Percy with Cypress, intercept image requests with cy.intercept() and return placeholder URLs before Percy takes the snapshot. This produces identical pixel output across Percy's Chromium instances regardless of CDN state.

Percy's --allowed-hostname flag controls which external domains Percy agents fetch. Add fallback.pics to the allowed list so Percy's rendering agent can fetch placeholder images directly.

Implementation text
// .percy.yml
version: 2
snapshot:
  widths: [375, 768, 1280]
  min-height: 600
  enable-javascript: true

# Allow Percy to fetch from fallback.pics
percy:
  allowed-hostnames:
    - fallback.pics

# cypress/support/percy-setup.ts
before(() => {
  cy.intercept('https://cdn.yourapp.com/**', {
    statusCode: 302,
    headers: {
      Location: 'https://fallback.pics/api/v1/400x300/7C3AED/FFFFFF?text=Percy+Test',
    },
  });
});

Chromatic

Chromatic TurboSnap and stable placeholder args

Chromatic's TurboSnap skips snapshots for stories whose dependency graph has not changed since the last build. Stories that use inline placeholder URLs (not fetched from a fixture file that changes) benefit maximally from TurboSnap: they only re-snapshot when the component file itself changes.

Configure Chromatic's --only-changed flag to further limit snapshots to affected stories. Combined with deterministic placeholder images, this can reduce your Chromatic build time by 60–80% on a large Storybook.

Implementation tsx
// package.json
{
  "scripts": {
    "chromatic": "chromatic --project-token=$CHROMATIC_PROJECT_TOKEN --only-changed",
    "chromatic:all": "chromatic --project-token=$CHROMATIC_PROJECT_TOKEN"
  }
}

// chromatic.config.ts
import { defineConfig } from 'chromatic';

export default defineConfig({
  projectId: 'your-project-id',
  // Prevent Chromatic from flagging external image changes
  externals: ['https://cdn.yourapp.com/**'],
  // Stable fallback.pics images don't need to be listed here
});

Decorator

Global Storybook decorator to replace image props

Write a Storybook decorator that intercepts network requests using MSW (Mock Service Worker) and returns placeholder images for all external image domains. This avoids the need to update every story when your CDN domain changes.

MSW's browser integration with Storybook's msw-storybook-addon lets you define handlers in a global preview.js file that apply to every story without per-story setup.

Implementation tsx
// .storybook/preview.ts
import { initialize, mswLoader } from 'msw-storybook-addon';
import { http, HttpResponse } from 'msw';

initialize();

export const loaders = [mswLoader];

export const parameters = {
  msw: {
    handlers: [
      http.get('https://cdn.yourapp.com/*', () => {
        return HttpResponse.redirect(
          'https://fallback.pics/api/v1/400x300/E5E7EB/71717A?text=Mocked',
          302
        );
      }),
    ],
  },
};

Key takeaways

What to standardize before shipping

  • Real CDN image URLs in Storybook args or Percy fixtures cause false-positive visual regression failures; deterministic placeholders eliminate image-related noise entirely.
  • Replace story imageUrl args with fallback.pics URLs that match the component's expected image dimensions and color scheme.
  • Percy's allowed-hostname configuration must include fallback.pics for Percy rendering agents to fetch placeholder images during snapshot capture.
  • Chromatic TurboSnap skips unchanged stories more effectively when image URLs are inline in story args rather than in frequently-changing fixture files.
  • Use MSW in Storybook's preview.ts to globally intercept CDN image requests without updating every story individually.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs