Configuration
Happo looks for configuration in a happo.config.{js,ts,mjs,cjs,mts,cts} file in your current working directory. You can override this path using the --config CLI option or the HAPPO_CONFIG_FILE environment variable. The config file can use CommonJS or ES modules syntax. The configuration file can export either an object containing configuration options or an (async) function that resolves to configuration options.
apiKey and apiSecret
Available since happo v6.0.0.
These tokens authenticate you with happo.io. Never store these tokens in plain text. Use environment variables instead.
import { defineConfig } from 'happo';
export default defineConfig({
apiKey: process.env.HAPPO_API_KEY,
apiSecret: process.env.HAPPO_API_SECRET,
});
Note: If
apiKeyandapiSecretare not provided, the CLI will attempt to authenticate interactively by opening a browser. This interactive authentication creates short-lived tokens and will not work in CI environments or non-interactive terminals. For CI and production use, you must provide explicitapiKeyandapiSecretvalues.
targets
Available since happo v6.0.0.
Specify the browsers you want to include in your happo run. For example:
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
// The first part ('firefox-desktop' in this case) is a name you give
// to the specific browser target. You'll see this name in the reports
// generated during a happo run.
'firefox-desktop': {
type: 'firefox',
viewport: '1024x768',
},
'firefox-mobile': {
type: 'firefox',
viewport: '320x640',
},
chrome: {
type: 'chrome',
viewport: '800x600',
},
edge: {
type: 'edge',
viewport: '800x600',
},
accessibility: {
type: 'accessibility',
viewport: '1024x768',
},
},
});
Viewport sizes can range from 300x300 to 2000x2000. The ios-safari target type runs on an iPhone with a fixed viewport of 375x667. The ipad-safari target type is always 1080x810.
Supported types:
firefoxchromeedgesafariios-safari(runs on iPhone 7)ipad-safari(runs on iPad)accessibility
Target freezeAnimations
Available since happo v6.0.0.
By default, Happo freezes CSS animations on the last frame. To freeze animations on the first frame instead (legacy behavior), use the freezeAnimations option:
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
ie: {
type: 'edge',
viewport: '1024x768',
freezeAnimations: 'first-frame',
},
},
});
To capture how something moves instead of freezing it, see animate.
Target animate
Available since happo v6.15.0.
Capture animations as animated PNGs instead of freezing them. 'auto' captures an animated snapshot only when the page has an animation Happo can drive, and takes an ordinary still image otherwise, so it's safe to enable for a whole target:
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
animate: 'auto',
},
},
});
animate can also be set on individual Storybook stories and pages, and takes an object with options for frame rate, duration, triggers, and more. See Animated snapshots for the full list of options.
Target chunks
Available since happo v6.0.0. Automatic chunk sizing available since happo v6.4.1.
As of v6.4.1, Happo automatically sets the number of chunks based on an estimated snapshot count when using the Storybook integration. Most projects don't need to configure this manually.
If you are using the custom integration type, automatic chunk sizing requires returning estimatedSnapsCount from your build function:
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'custom',
build: async () => ({
rootDir: './tmp/happo-custom',
entryPoint: 'bundle.js',
estimatedSnapsCount: 42,
}),
},
});
Without this value, Happo cannot auto-chunk custom integrations and you will need to set chunks explicitly.
If you want to override the automatic behavior, use the chunks option to explicitly split a target into multiple parallel workers:
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
chunks: 2,
},
},
});
Happo.io attempts to run chunks in parallel, but there's no guarantee. The chunks option adds some overhead, so if your test suite isn't large, using more than one chunk might actually slow things down.
When to set chunks manually:
- Storybook with many interaction tests: The automatic estimate is based on story count and does not account for interaction tests. If you use the Storybook integration and have a large number of interaction tests, the estimate may be too low and you may benefit from setting a higher chunk count explicitly.
- Consistently slow runs: If happo runs are taking longer than expected and parallelism seems insufficient, try increasing chunks.
- Reducing parallelism: If you want to limit resource usage, you can set
chunks: 1to disable splitting entirely.
Target maxHeight
Available since happo v6.0.0.
Use maxHeight to override the default maximum height used by Happo workers (5000 pixels). This is useful when taking screenshots of tall components or pages. For example:
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
maxHeight: 10000,
},
},
});
Note: The maximum width defaults to the maximum height, so if you set maxHeight, you may also want to set maxWidth at the same time.
Target maxWidth
Available since happo v6.0.0.
Use maxWidth to override the default maximum width used by Happo workers (defaults to maxHeight, which defaults to 5000 pixels). This is useful when taking screenshots of wide components or pages. For example:
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
maxWidth: 10000,
},
},
});
Target hideBehavior
Available since happo v6.0.0.
This option controls how Happo handles elements with the data-happo-hide attribute. By default, elements with this attribute are made invisible. Use the value ignore to make the content appear in screenshots but exclude it from comparison.
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
safari: {
type: 'safari',
viewport: '1024x768',
hideBehavior: 'ignore',
},
},
});
Target useFullPageFallbackForTallScreenshots
Available since happo v6.0.0.
This option applies to Chrome and Firefox only.
When Chrome and Firefox workers take screenshots of pages taller than 4000 pixels, they apply a workaround that briefly resizes the viewport so all content fits inside it. Without this workaround, content below the viewport's bottom edge can disappear inconsistently. However, this workaround can cause other issues, especially when using the vh CSS unit. A page with an element of height: 100vh will take up the entire screenshot when the viewport-altering fallback is active. To disable this workaround completely, set useFullPageFallbackForTallScreenshots: false.
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
useFullPageFallbackForTallScreenshots: false,
},
firefox: {
type: 'firefox',
viewport: '1024x768',
useFullPageFallbackForTallScreenshots: false,
},
},
});
Target applyPseudoClasses
Available since happo v6.0.0.
When set to true, this option allows you to add data-happo-hover, data-happo-focus, data-happo-focus-visible, and data-happo-active attributes to your DOM elements and have Happo apply the corresponding :hover, :focus, :focus-visible, or :active styles. For example, if you have this markup:
<button>Hover me</button>
<style>
button:hover {
background-color: blue;
}
</style>
To apply the hover style before taking the screenshot (making the button blue), change the markup to:
<button data-happo-hover>Hover me</button>
<style>
button:hover {
background-color: blue;
}
</style>
Similarly, you can add focus to elements using data-happo-focus:
<input type="text" data-happo-focus />
data-happo-focus focuses the element the same way a mouse click or a programmatic element.focus() call does, which means :focus styles are applied but :focus-visible styles are not.
To capture the focus ring that keyboard users see, use data-happo-focus-visible instead. It focuses the element as if the user had tabbed to it, so both :focus and :focus-visible styles are applied:
<button data-happo-focus-visible>Tab to me</button>
<style>
button:focus {
background-color: lightblue;
}
button:focus-visible {
outline: 2px solid blue;
}
</style>
Use one or the other on a given snapshot — since only one element can hold focus at a time, Happo focuses the first element it finds with data-happo-focus, then the first element with data-happo-focus-visible, so the latter wins if both are present.
And add data-happo-active to elements to simulate the :active state:
<button data-happo-active>Click me</button>
<style>
button:active {
background-color: red;
}
</style>
Target prefersColorScheme
Available since happo v6.0.0.
Set prefersColorScheme: 'dark' or prefersColorScheme: 'light' to set the color scheme preference in the browser.
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
prefersColorScheme: 'dark',
},
},
});
When enabled, styles affected by the color scheme will be activated:
background: white;
color: black;
@media (prefers-color-scheme: dark) {
background: black;
color: white;
}
Target interfaceOrientation
Set interfaceOrientation: 'landscape' to rotate the device before taking screenshots. The default is 'portrait'.
Note: This option only applies to ios-safari and ipad-safari targets.
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
'ipad-landscape': {
type: 'ipad-safari',
interfaceOrientation: 'landscape',
},
},
});
Target prefersReducedMotion
Available since happo v6.0.0.
By default, Happo is configured to prefer reduced motion. Set this option to false to disable this behavior.
Note: This option has no effect in iOS Safari.
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
prefersReducedMotion: false,
},
},
});
When true (default behavior), media queries that use prefers-reduced-motion: reduce will be activated:
@media (prefers-reduced-motion: reduce) {
button {
animation: none;
}
}
To override this for animated snapshots only, set prefersReducedMotion inside animate instead. See reduced motion.
Target allowPointerEvents
Available since happo v6.0.0. Default flipped to true in happo v6.8.0.
Since v6.8.0, pointer events are allowed by default. Happo no longer injects CSS to disable pointer events, so mouse interactions in tests work without any extra configuration.
If you notice unexpected diffs caused by hover effects, you can set allowPointerEvents: false to restore the previous behavior:
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
allowPointerEvents: false,
},
},
});
:::note Before v6.8.0, the default was allowPointerEvents: false, which caused Happo to inject this CSS:
* {
pointer-events: none !important;
}
This prevented spurious hover effects but also caused Storybook interaction tests that relied on pointer events to be silently skipped. :::
If you're interested in testing hover, focus, and active states with Happo, you may also want to use the applyPseudoClasses option.
Target outgoingRequestHeaders
Available since happo v6.0.0.
Add additional headers to outgoing requests from the browser. This is useful if you need to tell a CDN or other service that the request originates from a Happo run, or if you need to pass authentication headers.
Note: This option only applies to desktop browsers (Chrome, Firefox, Edge, Safari, and accessibility).
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
outgoingRequestHeaders: [
{ name: 'X-Happo-Run', value: 'true' },
{ name: 'Authorization', value: 'Bearer token123' },
],
},
},
});
Target allowedHostnames
Available since happo v6.19.0.
Restrict which hostnames the browser is allowed to make HTTP(S) requests to while rendering. Anything not covered by the list is refused before it leaves the browser.
A snapshot that loads a font, a script, or an image from somewhere else changes when that somewhere else does, and fails when it is down. The fewer hostnames a target needs, the more reproducible its snapshots are.
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
allowedHostnames: ['fonts.gstatic.com', '*.mycdn.example.com'],
},
},
});
Entries are hostnames, matched exactly. Prefix one with *. to cover subdomains: *.example.com covers cdn.example.com but not example.com itself, so list both if you need both. Ports and paths are ignored, so https://example.com/assets and example.com mean the same thing.
Requests to the Happo worker's own server — the page being rendered and everything in your uploaded package — are always allowed and don't belong in the list. Neither do data: and blob: URLs, which never hit the network.
Leaving the option unset is not the same as setting it to an empty array:
- Unset (the default) blocks nothing.
allowedHostnames: []blocks every external request. This is a good goal for a self-contained test suite, and a good way to find out what your snapshots depend on.
You don't have to guess what to put in the list. Every run logs the external hostnames its pages reached for, whether or not a list is set, so you can run once without one and read them off the snap-request's logs in happo.io:
External requests: 12 to 2 hostnames: fonts.gstatic.com (x11), cdn.example.com
Once a list is in force, the same line splits into what was allowed and what was blocked. That's the line to read when a snapshot comes back missing something, and the report's logs page in happo.io opens with it rather than making you search for it. See Missing fonts, images, or other external assets for how to work through one.
Important: With the pages integration, the pages you screenshot are loaded over the network like any other external request. If you set allowedHostnames, you must include the hostnames of your page URLs, or those pages will no longer load and their snapshots will fail.
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
// 'example.com' is here because the pages below are served from it
allowedHostnames: ['example.com', 'fonts.gstatic.com'],
},
},
integration: {
type: 'pages',
pages: [
{ url: 'https://example.com/home', title: 'Home Page' },
{ url: 'https://example.com/about', title: 'About Page' },
],
},
});
Note: This option is experimental. Its shape may change in a future release.
Note: This option only applies to desktop browsers (Chrome, Firefox, Edge, Safari, and accessibility). It is not supported on ios-safari or ipad-safari, where the browser can't be pointed at the proxy that does the blocking. Those targets log that the option had no effect rather than leaving you to assume that it did.
project
Available since happo v6.0.0.
If you have multiple projects configured for your happo.io account, specify the name of the project you want to associate with. If left empty, the default project will be used.
import { defineConfig } from 'happo';
export default defineConfig({
project: 'my-project-name',
// ... rest of config
});
deepCompare
Available since happo v6.3.0.
Override the project-level deep-compare settings for comparisons started from this configuration file. Omit this option to use the project defaults.
See the Compare with a threshold guide for a full explanation of each setting.
import { defineConfig } from 'happo';
export default defineConfig({
deepCompare: {
compareThreshold: 0.002,
diffAlgorithm: 'color-delta',
ignoreThreshold: 0,
ignoreWhitespace: false,
applyBlur: false,
},
// ... rest of config
});
| Option | Type | Default | Description |
|---|---|---|---|
compareThreshold | number | — | How different two pixels are allowed to be (0–1). Required when using deepCompare. |
diffAlgorithm | 'color-delta' | 'ssim' | 'color-delta' | Algorithm used for pixel comparison. 'ssim' is experimental. |
ignoreThreshold | number | 0 | Fraction of pixels allowed to exceed compareThreshold before the screenshot is considered a diff (e.g. 0.01 = 1% of pixels). |
ignoreWhitespace | boolean | false | When true, whitespace-only differences are ignored. |
applyBlur | boolean | false | When true, a blur is applied before comparing to smooth out subtle edge differences. |
blockApproval
Available since happo v6.21.0.
Prevent a comparison from being approved until certain problems are resolved. Each condition is opt-in.
import { defineConfig } from 'happo';
export default defineConfig({
blockApproval: {
renderErrors: true,
accessibilityViolations: true,
},
// ... rest of config
});
| Option | Type | Description |
|---|---|---|
renderErrors | boolean | When true, approval is blocked while the report for the current commit has examples that failed to render. |
accessibilityViolations | boolean | When true, approval is blocked while the comparison introduces new accessibility violations. |
Because the conditions live in the configuration file, they apply to comparisons started from that file. You can turn one on in a single pull request, and fix the problems there, without affecting other work in the repository.
failOnWaitForTimeout
Available since happo v6.12.0.
Controls how Happo workers react when a waitForContent, waitForSelector, or waitFor option times out before the expected content, selector, or condition appears. Defaults to true.
true(default) — the snap fails with a clear error. This surfaces stalewaitForContentstrings,waitForSelectorvalues, andwaitForpredicates immediately, so they can be fixed at the source instead of silently adding multi-second waits to every run.false— the timeout only emits a warning in the worker logs and the screenshot is taken anyway against whatever happens to be on the page. This is the legacy behavior.
We recommend leaving this set to true. Set it to false as a temporary escape hatch while you investigate a failing run; the long-term fix is to update the offending wait so it matches the content that actually renders.
import { defineConfig } from 'happo';
export default defineConfig({
failOnWaitForTimeout: false,
// ... rest of config
});
integration
Specify the type of integration you're using with Happo. The integration type determines how Happo discovers and renders your components.
integration.type
Available since happo v6.0.0.
The type of integration. Supported values:
'storybook'- For Storybook integrations'cypress'- For Cypress E2E test integrations'playwright'- For Playwright E2E test integrations'custom'- For custom bundle integrations'pages'- For full-page screenshot integrations
Each integration has a different set of options that it supports.
Storybook Integration Options
integration.configDir
Available since happo v6.0.0.
The directory containing your Storybook configuration. Defaults to .storybook.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'storybook',
configDir: '.storybook',
},
// ... rest of config
});
integration.staticDir
Available since happo v6.0.0.
The directory containing static files to serve with Storybook. This corresponds to the staticDirs option in your Storybook configuration.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'storybook',
staticDir: './public',
},
// ... rest of config
});
integration.outputDir
Available since happo v6.0.0.
The directory to output the static Storybook package to. This is useful when using usePrebuiltPackage to specify where your prebuilt Storybook files are located.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'storybook',
outputDir: './storybook-static',
},
// ... rest of config
});
integration.usePrebuiltPackage
Available since happo v6.0.0.
When set to true, Happo will use a prebuilt Storybook package instead of building one. Make sure that files are built to the outputDir directory when using this option.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'storybook',
usePrebuiltPackage: true,
outputDir: './storybook-static',
},
// ... rest of config
});
integration.previewOnly
Available since happo v6.17.0.
Build the Storybook preview without the manager UI (Storybook's --preview-only), which typically makes the uploaded package several times smaller. Defaults to true.
Happo only ever loads iframe.html, so the manager is dead weight as far as rendering goes. Set this option to false if you download built packages and open them locally to debug: without the manager, a package is no longer a browsable Storybook, and reaching a story means visiting iframe.html?id=<storyId>&viewMode=story by hand.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'storybook',
previewOnly: false,
},
// ... rest of config
});
Note: This option is ignored on Storybook v8, which has no --preview-only flag. If you set previewOnly: true explicitly on v8, Happo logs that it was ignored and builds the package as usual.
integration.navigatePerStory
Available since happo v6.15.0.
When set to true, each story is rendered by navigating directly to iframe.html?id=<storyId> instead of loading iframe.html once and paging through stories client-side. Defaults to false.
This is slower, since it means one navigation per story, but it gives every story a fresh page load. That can help with Storybooks where state leaks between stories, e.g. through global CSS, singletons, or other side effects that the default in-page navigation doesn't reset.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'storybook',
navigatePerStory: true,
},
// ... rest of config
});
Note: This option requires an index.json or stories.json file in the built Storybook package. If that file is missing, Happo falls back to the default navigation strategy.
integration.skip
Available since happo v6.0.0.
Items to skip when generating snapshots. Can be an async function that resolves to an array of {component, variant}, or an array of {component, variant}.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'storybook',
skip: [
{ component: 'Button', variant: 'disabled' },
{ component: 'Modal', variant: 'large' },
],
},
// ... rest of config
});
Or as an async function:
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'storybook',
skip: async () => {
// Dynamically determine which items to skip
return [{ component: 'Button', variant: 'disabled' }];
},
},
// ... rest of config
});
Custom Integration Options
integration.build
Available since happo v6.0.0.
An async function that builds your custom bundle and returns an object with rootDir (path to the folder where files have been built) and entryPoint (local file name of the built JavaScript bundle).
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'custom',
build: async () => ({
rootDir: './tmp/happo-custom',
entryPoint: 'bundle.js',
}),
},
// ... rest of config
});
Cypress and Playwright Integration Options
integration.autoApplyPseudoStateAttributes
Available since happo v6.9.0.
Experimental
When set to true, Happo automatically collects and represents :focus, :focus-visible, :active, and :hover states in screenshots. At snapshot time, elements that are currently in one of those states get the corresponding data-happo-hover, data-happo-active, and data-happo-focus-visible attribute added for you. This lets you capture interactive states — including the keyboard focus ring — without manually adding data-happo-* attributes to your markup.
Note: Basic focus handling (data-happo-focus, based on the document's active element) is always applied, regardless of this option. Enabling autoApplyPseudoStateAttributes additionally traverses into shadow DOM to find the deepest focused element, and distinguishes keyboard focus (data-happo-focus-visible) from other focus.
This option requires the applyPseudoClasses target option.
import { defineConfig } from 'happo';
export default defineConfig({
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
applyPseudoClasses: true,
},
},
integration: {
type: 'playwright',
autoApplyPseudoStateAttributes: true,
},
// ... rest of config
});
integration.allowFailures
Available since happo v6.0.0.
When set to true, allows Happo tests to fail without causing the overall test run to fail. This is useful when you want to collect visual diffs without blocking your CI pipeline.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'cypress',
allowFailures: true,
},
// ... rest of config
});
Pages Integration Options
integration.pages
Available since happo v6.0.0.
A list of pages to screenshot. Each page object must include a url (the URL of the page to screenshot) and a title (used as the "component" identifier in Happo reports, so ensure it is unique for each page).
Optionally, you can specify waitForContent to wait for specific content to appear on the page before taking the screenshot, or waitForSelector to wait for a selector to appear in the document before taking the screenshot. If the content or selector does not appear within the worker's timeout, the snap fails by default. See failOnWaitForTimeout if you need to opt out of that behavior.
Note: The URLs to the website need to be publicly available, otherwise Happo workers won't be able to access the pages.
Note: If you use the allowedHostnames target option, the hostnames of these page URLs need to be in that list. They are loaded over the network like any other external request, so a list that leaves them out will stop these pages from loading.
import { defineConfig } from 'happo';
export default defineConfig({
integration: {
type: 'pages',
pages: [
{
url: 'https://example.com/home',
title: 'Home Page',
},
{
url: 'https://example.com/about',
title: 'About Page',
waitForContent: 'About Us',
},
{
url: 'https://example.com/products',
title: 'Products Page',
waitForSelector: '.product-list',
},
],
},
// ... rest of config
});
endpoint
Available since happo v6.0.0.
The endpoint to use for the Happo run (this is used for on-premise Happo). Defaults to https://happo.io.
import { defineConfig } from 'happo';
export default defineConfig({
endpoint: 'https://happo.my-company.com',
// ... rest of config
});
githubApiUrl
Available since happo v6.0.0.
Used when you have the CI script configured to post Happo statuses as comments. The default is https://api.github.com. If you're using GitHub Enterprise, enter the URL to your local GitHub API here, such as https://ghe.mycompany.zone/api/v3 (the default for GHE installation is for the API to be located at /api/v3).