Skip to main content

Chromatic vs Percy vs Applitools: Visual Regression Testing (2026)

·PkgPulse Team

TL;DR

Chromatic is the visual testing platform for Storybook — automatic snapshot testing, component-level diffing, UI review workflows, built by the Storybook team. Percy (by BrowserStack) is the visual review platform — pixel-by-pixel comparison, responsive testing, cross-browser, CI integration, works with any framework. Applitools is the AI-powered visual testing platform — Visual AI that ignores irrelevant differences, cross-browser, Ultrafast Grid, enterprise-grade visual validation. In 2026: Chromatic for Storybook component testing, Percy for cross-browser visual regression, Applitools for AI-powered visual validation.

Key Takeaways

  • Chromatic: chromatic ~200K weekly downloads — Storybook-native, component snapshots, UI review
  • Percy: @percy/cli ~100K weekly downloads — pixel comparison, responsive, cross-browser
  • Applitools: @applitools/eyes-cypress ~50K weekly downloads — Visual AI, Ultrafast Grid
  • Chromatic integrates natively with Storybook for component-level testing
  • Percy provides the most straightforward pixel-by-pixel comparison
  • Applitools uses AI to detect only meaningful visual changes

Chromatic

Chromatic — visual testing for Storybook:

Setup

npm install --save-dev chromatic

# First run (connect to Chromatic):
npx chromatic --project-token=YOUR_TOKEN

Storybook stories (test targets)

// src/components/PackageCard.stories.tsx
import type { Meta, StoryObj } from "@storybook/react"
import { PackageCard } from "./PackageCard"

const meta: Meta<typeof PackageCard> = {
  component: PackageCard,
  title: "Components/PackageCard",
  parameters: {
    chromatic: {
      viewports: [375, 768, 1200],  // Test at multiple viewports
      delay: 300,  // Wait 300ms before snapshot
    },
  },
}

export default meta
type Story = StoryObj<typeof PackageCard>

export const Default: Story = {
  args: {
    name: "react",
    description: "UI library for building interfaces",
    downloads: 25000000,
    version: "19.0.0",
    tags: ["frontend", "ui"],
  },
}

export const WithLongDescription: Story = {
  args: {
    ...Default.args,
    description: "A very long description that tests how the card handles overflow text in the description area when content exceeds the expected length",
  },
}

export const NoTags: Story = {
  args: {
    ...Default.args,
    tags: [],
  },
}

export const Loading: Story = {
  args: {
    ...Default.args,
    isLoading: true,
  },
}

// Interaction test (Chromatic captures after interaction):
export const Expanded: Story = {
  args: Default.args,
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)
    await userEvent.click(canvas.getByRole("button", { name: "Show Details" }))
  },
}

CI integration

# .github/workflows/chromatic.yml
name: Chromatic

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Required for Chromatic to detect changes

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - name: Run Chromatic
        uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          exitZeroOnChanges: true  # Don't fail CI on visual changes
          autoAcceptChanges: main  # Auto-accept on main branch
          onlyChanged: true  # Only test changed stories

Configuration

// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite"

const config: StorybookConfig = {
  stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-essentials"],
  framework: "@storybook/react-vite",
}

export default config

// Component-level Chromatic settings:
// Ignore specific stories:
export const AnimatedComponent: Story = {
  parameters: {
    chromatic: { disableSnapshot: true },  // Skip this story
  },
}

// Diff threshold:
export const SubtleChange: Story = {
  parameters: {
    chromatic: { diffThreshold: 0.2 },  // Allow 20% pixel difference
  },
}

// Pause animations:
export const WithAnimation: Story = {
  parameters: {
    chromatic: { pauseAnimationAtEnd: true },
  },
}

Percy

Percy — visual review platform:

Setup with Cypress

npm install --save-dev @percy/cli @percy/cypress
// cypress/e2e/visual.cy.ts
describe("Package Comparison Page", () => {
  it("renders the comparison table", () => {
    cy.visit("/compare?packages=react,vue")
    cy.get("[data-testid=comparison-table]").should("be.visible")
    cy.percySnapshot("Comparison Table")
  })

  it("renders dark mode", () => {
    cy.visit("/compare?packages=react,vue")
    cy.get("[data-testid=theme-toggle]").click()
    cy.percySnapshot("Comparison Table - Dark Mode")
  })

  it("renders responsive layouts", () => {
    cy.visit("/compare?packages=react,vue")
    cy.percySnapshot("Comparison - Desktop", { widths: [1280] })
    cy.percySnapshot("Comparison - Tablet", { widths: [768] })
    cy.percySnapshot("Comparison - Mobile", { widths: [375] })
  })

  it("renders loading state", () => {
    cy.intercept("GET", "/api/packages/*", { delay: 10000 }).as("slowApi")
    cy.visit("/compare?packages=react,vue")
    cy.percySnapshot("Comparison - Loading State")
  })
})

Setup with Playwright

npm install --save-dev @percy/cli @percy/playwright
// tests/visual.spec.ts
import { test } from "@playwright/test"
import percySnapshot from "@percy/playwright"

test.describe("Visual regression tests", () => {
  test("homepage", async ({ page }) => {
    await page.goto("/")
    await page.waitForSelector("[data-testid=package-list]")
    await percySnapshot(page, "Homepage")
  })

  test("package detail page", async ({ page }) => {
    await page.goto("/packages/react")
    await page.waitForSelector("[data-testid=package-detail]")
    await percySnapshot(page, "Package Detail - React")
  })

  test("search results", async ({ page }) => {
    await page.goto("/")
    await page.fill("[data-testid=search-input]", "state management")
    await page.waitForSelector("[data-testid=search-results]")
    await percySnapshot(page, "Search Results - State Management")
  })

  test("responsive comparison", async ({ page }) => {
    await page.goto("/compare?packages=react,vue,svelte")

    // Percy handles responsive widths:
    await percySnapshot(page, "Three-way Comparison", {
      widths: [375, 768, 1024, 1440],
    })
  })
})

CI integration

# .github/workflows/percy.yml
name: Percy Visual Tests

on:
  pull_request:
    branches: [main]

jobs:
  visual-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: Percy with Playwright
        run: npx percy exec -- npx playwright test tests/visual/
        env:
          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

      # Percy Configuration:
      # .percy.yml
      # version: 2
      # snapshot:
      #   widths: [375, 768, 1280]
      #   minHeight: 1024
      #   percyCSS: |
      #     .animated { animation: none !important; }
      #     [data-testid="timestamp"] { visibility: hidden; }

Storybook integration

npm install --save-dev @percy/storybook
# Snapshot all Storybook stories:
npx percy storybook ./storybook-static

# With filtering:
npx percy storybook ./storybook-static \
  --include="Components/**" \
  --exclude="**/Playground*"
// In stories — Percy-specific parameters:
export const Default: Story = {
  args: { /* ... */ },
  parameters: {
    percy: {
      widths: [375, 768, 1280],
      skip: false,
      additionalSnapshots: [
        { name: "Dark Mode", args: { theme: "dark" } },
      ],
    },
  },
}

Applitools

Applitools — AI-powered visual testing:

Setup with Cypress

npm install --save-dev @applitools/eyes-cypress
npx eyes-setup
// cypress/e2e/visual.cy.ts
describe("Visual AI Testing", () => {
  beforeEach(() => {
    cy.eyesOpen({
      appName: "PkgPulse",
      batchName: "Package Comparison",
    })
  })

  afterEach(() => {
    cy.eyesClose()
  })

  it("validates comparison page layout", () => {
    cy.visit("/compare?packages=react,vue")
    cy.get("[data-testid=comparison-table]").should("be.visible")

    // Visual AI check — ignores irrelevant differences:
    cy.eyesCheckWindow({
      tag: "Comparison Table",
      target: "window",
      fully: true,  // Capture full page
    })
  })

  it("validates with different match levels", () => {
    cy.visit("/packages/react")

    // Strict — pixel-perfect:
    cy.eyesCheckWindow({
      tag: "Package Detail - Strict",
      matchLevel: "Strict",
    })

    // Layout — only structure matters:
    cy.eyesCheckWindow({
      tag: "Package Detail - Layout",
      matchLevel: "Layout",
    })

    // Content — text content must match:
    cy.eyesCheckWindow({
      tag: "Package Detail - Content",
      matchLevel: "Content",
    })
  })

  it("validates responsive across browsers", () => {
    cy.visit("/")

    // Ultrafast Grid — test across browsers and viewports:
    cy.eyesCheckWindow({
      tag: "Homepage",
      target: "window",
      fully: true,
    })
  })
})

// applitools.config.js
module.exports = {
  testConcurrency: 5,
  browser: [
    // Ultrafast Grid — all tested in parallel:
    { width: 1200, height: 800, name: "chrome" },
    { width: 1200, height: 800, name: "firefox" },
    { width: 1200, height: 800, name: "safari" },
    { width: 768, height: 1024, name: "chrome" },
    { width: 375, height: 812, name: "chrome", deviceName: "iPhone 14" },
  ],
  batchName: "PkgPulse Visual Tests",
}

Setup with Playwright

npm install --save-dev @applitools/eyes-playwright
// tests/visual-ai.spec.ts
import { test } from "@playwright/test"
import { BatchInfo, Configuration, Eyes, Target } from "@applitools/eyes-playwright"

let eyes: Eyes

test.beforeAll(async () => {
  const config = new Configuration()
  config.setBatch(new BatchInfo("PkgPulse"))
  config.addBrowser(1200, 800, "chrome")
  config.addBrowser(1200, 800, "firefox")
  config.addBrowser(768, 1024, "chrome")
  config.addDeviceEmulation("iPhone 14")

  eyes = new Eyes()
  eyes.setConfiguration(config)
})

test.afterAll(async () => {
  await eyes.abortAsync()
})

test("homepage visual validation", async ({ page }) => {
  await eyes.open(page, "PkgPulse", "Homepage")

  await page.goto("/")
  await page.waitForSelector("[data-testid=package-list]")

  // AI check — full page:
  await eyes.check("Homepage", Target.window().fully())

  // Check specific region:
  await eyes.check(
    "Navigation",
    Target.region("[data-testid=navbar]")
  )

  // Check with floating regions (ignore dynamic content):
  await eyes.check(
    "Package List",
    Target.region("[data-testid=package-list]")
      .floating({ element: "[data-testid=download-count]", maxOffset: { top: 5, bottom: 5 } })
      .ignore("[data-testid=timestamp]")
  )

  await eyes.close()
})

test("dark mode validation", async ({ page }) => {
  await eyes.open(page, "PkgPulse", "Dark Mode")

  await page.goto("/")
  await page.click("[data-testid=theme-toggle]")

  await eyes.check("Dark Mode Homepage", Target.window().fully())
  await eyes.close()
})

CI integration

# .github/workflows/applitools.yml
name: Applitools Visual Tests

on:
  pull_request:
    branches: [main]

jobs:
  visual-ai:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: Run Applitools tests
        run: npx playwright test tests/visual-ai/
        env:
          APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}

Feature Comparison

FeatureChromaticPercyApplitools
Testing approachComponent (Storybook)Page/componentPage/component
Comparison methodPixel diffPixel diffVisual AI
Storybook integration✅ (native)✅ (addon)✅ (addon)
Playwright supportVia Storybook✅ (native)✅ (native)
Cypress supportVia Storybook✅ (native)✅ (native)
Cross-browserChrome onlyChrome, Firefox, Safari100+ (Ultrafast Grid)
Responsive testing✅ (viewports)✅ (widths)✅ (devices + browsers)
AI-powered✅ (Visual AI)
Ignore regions✅ (AI auto-detect)
Match levelsPixelPixelStrict, Layout, Content
UI review workflow✅ (built-in)✅ (dashboard)✅ (dashboard)
PR integration✅ (GitHub)✅ (GitHub, GitLab)✅ (GitHub, GitLab)
Free tier5K snapshots/month5K snapshots/monthFree for OSS
PricingSnapshot-basedSnapshot-basedCheckpoint-based

When to Use Each

Use Chromatic if:

  • Using Storybook for component development
  • Want component-level visual testing integrated with stories
  • Need UI review workflows for design approvals
  • Prefer the tightest Storybook integration

Use Percy if:

  • Need page-level visual regression with any testing framework
  • Want straightforward pixel-by-pixel comparison
  • Building cross-browser visual tests with Playwright or Cypress
  • Prefer a simple, framework-agnostic visual testing solution

Use Applitools if:

  • Need AI-powered visual testing that ignores irrelevant differences
  • Want to test across 100+ browser/device combinations
  • Building enterprise applications that need comprehensive visual validation
  • Prefer Layout match level to avoid false positives from dynamic content

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on chromatic v11.x, @percy/cli v1.x, and @applitools/eyes-cypress v3.x.

Compare testing tools and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.