Skip to content

Configuration

The cappa.config.ts file controls how screenshots are captured, stored, and reviewed. Config files export a configuration created with defineConfig from @cappa/core.

defineConfig accepts either an object or a function that receives a ConfigEnv object with the current CLI command, mode, and environment variables. This is useful when you need to tailor the configuration in CI environments.

import { defineConfig } from '@cappa/core';
import { cappaPluginStorybook } from '@cappa/plugin-storybook';
export default defineConfig(({ env }) => ({
outputDir: env.CAPPA_SCREENSHOT_DIR ?? 'screenshots',
}));
  • type: string
  • default: 'screenshots'

Folder where screenshots and diffs are written. Cappa will create actual, expected, and diff directories within this folder.

You most probably want to exclude the actual and diff directories from your version control system.

  • type: number
  • default: 2

The number of times to retry a screenshot if it fails. See the Screenshot retries guide for more information.

  • type: number
  • default: 1

The number of parallel browser contexts to use when taking screenshots. Each context can process screenshots independently, significantly speeding up large screenshot runs.

Higher values will consume more system resources (CPU and memory).

  • type: boolean | 'error' | 'warn' | 'info' | 'log' | 'debug'
  • default: true

Controls which browser console events are logged while captures run.

  • true logs every console message at the CLI’s debug level (only visible with --log-level 4 or higher).
  • false disables console logging.
  • A severity ('error', 'warn', 'info', 'log', 'debug') logs only messages at or above it, at the matching CLI level. 'error' surfaces console.error / console.assert failures without raising the log level; 'warn' adds console.warn.

Uncaught page errors are always logged — as errors when a severity is set, otherwise at debug level.

export default defineConfig({
logConsoleEvents: 'error',
});

Screenshot configuration might get overwritten by plugins or more specific settings, while taking the screenshot.

  • type: { width: number; height: number }
  • default: { width: 1920, height: 1080 }

The viewport to use for the screenshots.

  • type: boolean
  • default: true

Whether to take a full page screenshot. If false, the viewport will be used.

If a height is defined, this setting will supersede the viewport height.

Diff configuration sets global comparison settings for screenshots.

diff supports two algorithms via diff.type:

  • "pixel" (default) for ‘traditional’ pixel-count based comparisons
  • "gmsd" for ‘gradient magnitude similarity deviation’ comparisons. Measures gradient similarity, not just pixel differences. Closer to human perception of similarity.
  • type: "pixel" | "gmsd"
  • default: "pixel"

Selects which algorithm is used when comparing screenshots.

  • type: number
  • default: 0.1
  • range: 0-1

This affects how similar a pixel needs to be to the reference image to be considered the same.

The following options apply when diff.type is "pixel".

  • type: boolean
  • default: false

Whether to include anti-aliased pixels in the diff count. For most users this should be false, because it makes comparisons very sensitive to sub-pixel differences of fonts and other UI elements.

  • type: boolean
  • default: true

Whether to use a fast buffer comparison for identical images. This is a performance optimization that skips the slower image comparison algorithm.

  • type: number
  • default: 0

The maximum number of different pixels before the comparison fails.

  • type: number
  • default: 0
  • range: 0-100

The maximum percentage of different pixels before the comparison fails. This is a relative value, so it is affected by the size of the images. A 100px image with a 10% difference is 10 pixels different, but a 1000px image with a 10% difference is 100 pixels different. On very large full page screenshots this can mean that small changes still slip through.

  • type: boolean
  • default: false

Run a structured interpretation pass after the raw pixel diff. Instead of only telling you where pixels differ, interpretation describes what changed — additions, deletions, color shifts and content changes — grouped into regions, each with a position, a percentage and a confidence score, plus an overall severity (Low / Medium / High) and a human-readable summary.

export default defineConfig({
diff: {
type: 'pixel',
interpret: true,
},
});

When enabled, each changed screenshot gets a diff/<name>.json sidecar next to its diff image:

{
"numDiffPixels": 17904,
"percentDifference": 1.87,
"interpretation": {
"summary": "Moderate visual change detected (1.87% of image, 3 regions). Content changed: 1 region (center).",
"severity": "Medium",
"totalRegions": 3,
"diffPercentage": 1.87,
"width": 1200,
"height": 800,
"regions": [
{
"changeType": "ContentChange",
"position": "center",
"percentage": 0.9,
"bbox": { "x": 420, "y": 300, "width": 360, "height": 200 }
}
]
}
}

The sidecar is read back by cappa capture, cappa status and the review UI:

  • cappa capture prints the same breakdown after the failure report, so CI logs show what changed and where without downloading the diff images.
  • cappa status prints a per-screenshot breakdown of changed screenshots with severity, region count, summary and the individual regions.
  • The review UI shows a severity badge, a summary banner and interactive, color-coded region overlays on the diff view.

The sidecar lives in the diff directory, so it is cleared before each capture and removed when a screenshot is approved.

The following options apply when diff.type is "gmsd".

  • type: 0 | 1
  • default: 0

Downsample factor used by GMSD.

  • type: number
  • default: 170

Stability constant used in the GMSD equation.

Generated PNG files include textual metadata (tEXt chunks) with the comparison details. This includes:

  • cappa.diff.algorithm (pixel or gmsd)
  • the configured diff options used for that comparison, stored as cappa.diff.<option>

You can read these fields in custom tooling to audit how a diff image was produced.

Configuration for the review UI.

  • type: 'light' | 'dark'
  • default: 'light'

Color theme for the review UI. Set to 'dark' for dark mode.

export default defineConfig({
review: {
theme: 'dark',
},
});
  • type: number
  • default: 3000

Port for the review UI server. Set to a different value if port 3000 is already in use.

export default defineConfig({
review: {
port: 4000,
},
});
  • type: number
  • default: 300000 (5 minutes)

How long the browser stays alive between captures started from the interactive UI, in milliseconds.

Keeping it warm makes an interactive capture feel immediate instead of paying browser startup on every click; evicting it once idle stops a forgotten review session holding a browser process indefinitely. Set it to 0 to shut the browser down after every run.

This only affects cappa review — cappa capture starts and stops a browser per invocation either way.

export default defineConfig({
review: {
browserIdleTimeout: 60_000,
},
});
  • type: WebServerConfig | WebServerConfig[]
  • default: undefined

The app under test — a Storybook, a preview server — that Cappa starts before capturing and stops when it is done. The URL already lives in your plugin config; this puts the command that brings it up next to it, so neither you, your CI pipeline nor a coding agent has to start it by hand.

Not to be confused with Cappa’s own review server (cappa review, cappa serve, review.port): webServer is what the plugins point at.

import { defineConfig } from '@cappa/core';
import { cappaPluginStorybook } from '@cappa/plugin-storybook';
export default defineConfig({
webServer: {
command: 'pnpm storybook --ci',
url: 'http://localhost:6006',
},
plugins: [cappaPluginStorybook({ storybookUrl: 'http://localhost:6006' })],
});
Field Default
command — Shell command that starts the server. Runs through a shell, so && works on Windows too.
url — Ready once it answers with a 2xx or 3xx status.
reuseExistingServer !process.env.CI Use a server that is already answering at url instead of running command. When false, an answering url is an error.
timeout 60000 How long url may take to become ready, in milliseconds, measured from when command starts.
cwd directory of cappa.config.ts Where command runs. A relative path resolves against the config file’s directory, not the current directory.
env {} Environment variables, merged over the inherited ones.

Pass an array to start several servers. They start in parallel, and all of them must be ready before anything is captured — including for a --filter run, since Cappa does not know which plugin needs which server.

The server starts on the first capture, not when Cappa starts, and every run logs which way it went: Reusing server at … or Started \…``. When screenshots look like a stale build or another branch, that line tells you whether Cappa ran your command at all.

  • cappa capture stops the server when the capture ends.
  • cappa review, cappa serve and cappa capture --watch keep it running alongside the warm browser, and stop it after the same review.browserIdleTimeout. The next capture starts it again — and rebuilds, so it picks up your changes.
  • With cappa capture --server, the host starts the server, from its own config.

A reused server is never stopped: Cappa did not start it. A server that Cappa started is stopped together with every process it spawned, so nothing is left holding the port. If it exits during a run, the captures that fail because of it say so — with the end of its output — and the next run starts it again.

The server’s output is kept out of the terminal. It is shown when the server fails to start or does not become ready in time, and streamed live with --log-level 4.

command can build first. timeout covers the build, so raise it:

export default defineConfig({
webServer: {
command: 'pnpm build-storybook && pnpm dlx http-server storybook-static -p 6006 --silent',
url: 'http://localhost:6006',
timeout: 180_000,
},
});

Reusing a server skips the whole command, build included. Locally that means an already running Storybook is captured as it is. Use a function config to pick a dev server locally and a static build in CI:

export default defineConfig(({ env }) => ({
webServer: env.CI
? {
command: 'pnpm build-storybook && pnpm dlx http-server storybook-static -p 6006 --silent',
url: 'http://localhost:6006',
timeout: 180_000,
}
: { command: 'pnpm storybook --ci', url: 'http://localhost:6006' },
}));

When cappa.config.ts lives in one package and the app in another, let the package manager find the app. This does not depend on the folder layout, and works the same with npm -w, yarn workspace or turbo:

webServer: {
command: 'pnpm -F @acme/web storybook --ci',
url: 'http://localhost:6006',
},

Or run the package’s own script from its directory:

webServer: {
command: 'pnpm storybook --ci',
cwd: '../../apps/web',
url: 'http://localhost:6006',
},

Both work whether Cappa runs from the repository root or from inside the package.

Turborepo’s interactive UI expects a terminal, so pass --ui=stream (turbo run storybook --filter=@acme/web --ui=stream) or call the package manager directly.

When a capture cannot connect to the app at all, Cappa reports it once rather than once per task, and says what to do: start it, or add a webServer if there is none.

  • type: (screenshots: FailedScreenshot[]) => void | Promise<void>
  • default: undefined

Runs after a capture completes in CI mode when there are screenshots that failed comparison. CI mode is enabled by running cappa capture --ci or by setting CI=true in the environment. The callback receives an array of failing screenshots, including both relative and absolute paths to the actual, expected, and diff images. Use this hook to upload diff images to object storage or integrate with CI systems.

import { awsS3Upload } from 'aws-sdk';
onFail: async (screenshots) => {
// Upload diff images to your storage provider
for (const screenshot of screenshots) {
if (screenshot.absoluteDiffPath) {
await awsS3Upload(screenshot.absoluteDiffPath);
}
}
}

Browser configuration sets different browser settings for playwright.

  • type: 'chromium' | 'firefox' | 'webkit'
  • default: 'chromium'

The browser to use for the screenshots.

  • type: boolean
  • default: true

Whether to run the browser in headless mode.