Happo: Cross-Browser Visual Testing for UI Components
Happo solves a specific problem: visual regression testing across multiple browsers simultaneously. While Chromium-only tools catch 80% of visual regressions, cross-browser differences — font rendering, flexbox quirks, Safari's date inputs — require testing against real browser engines. Happo runs your components in Chrome, Firefox, Safari, and Edge and compares screenshots across all of them.
What Makes Happo Different
Most visual regression tools (Percy, BackstopJS, Lost Pixel) use a single browser engine — typically Chromium. Happo captures screenshots in multiple real browsers and compares:
- Previous version vs current version — catch regressions in your primary browser
- Cross-browser — verify consistency between Chrome and Safari, for example
The second comparison is especially valuable for design systems and component libraries where you need to guarantee visual consistency across browsers.
Architecture
Happo has two parts:
happo-e2e— CLI tool that captures screenshotshappo.io— hosted service that stores screenshots and runs comparisons
You run screenshots locally or in CI, upload to happo.io, and the service handles storage, diffing, and review UI.
Installation
npm install --save-dev happo.jsYou need a Happo account for the API key. For open-source projects, Happo offers free plans.
Create .happo.js:
const { RemoteBrowserTarget } = require('happo.js');
module.exports = {
apiKey: process.env.HAPPO_API_KEY,
apiSecret: process.env.HAPPO_API_SECRET,
targets: {
'chrome-desktop': new RemoteBrowserTarget('chrome', {
viewport: '1024x768',
}),
'firefox-desktop': new RemoteBrowserTarget('firefox', {
viewport: '1024x768',
}),
'safari-desktop': new RemoteBrowserTarget('safari', {
viewport: '1024x768',
}),
'chrome-mobile': new RemoteBrowserTarget('chrome', {
viewport: '375x812',
}),
},
project: 'my-component-library',
};Writing Happo Examples
Happo uses "examples" — components rendered in isolation. For React:
// src/components/Button/Button-happo.jsx
export const primary = () => (
<Button variant="primary">Click me</Button>
);
export const secondary = () => (
<Button variant="secondary">Click me</Button>
);
export const disabled = () => (
<Button variant="primary" disabled>
Disabled
</Button>
);
export const loading = () => (
<Button variant="primary" loading>
Loading...
</Button>
);
export const allSizes = () => (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
);Happo discovers files matching *-happo.{js,jsx,tsx} by default. Configure in .happo.js:
module.exports = {
// ...
include: '**/*-happo.{js,jsx,tsx}',
// or
include: 'src/**/*.happo.{js,jsx,tsx}',
};Storybook Integration
For projects already using Storybook, use happo-plugin-storybook to reuse stories as Happo examples:
npm install --save-dev happo-plugin-storybook// .happo.js
const { RemoteBrowserTarget } = require('happo.js');
const happoPluginStorybook = require('happo-plugin-storybook');
module.exports = {
apiKey: process.env.HAPPO_API_KEY,
apiSecret: process.env.HAPPO_API_SECRET,
targets: {
chrome: new RemoteBrowserTarget('chrome', { viewport: '1024x768' }),
firefox: new RemoteBrowserTarget('firefox', { viewport: '1024x768' }),
safari: new RemoteBrowserTarget('safari', { viewport: '1024x768' }),
},
plugins: [
happoPluginStorybook({
outputDir: 'storybook-static',
}),
],
};Build your Storybook and run Happo:
npm run build-storybook
npx happo runEvery story becomes a Happo screenshot, tested across all configured browser targets.
Running Tests
# Take screenshots and upload to happo.io
npx happo run
<span class="hljs-comment"># Compare two commits
npx happo compare <sha1> <sha2>
<span class="hljs-comment"># Check comparison status (exits 0 if no differences)
npx happo check --<span class="hljs-built_in">link <happo-comparison-url>CI Integration
The typical workflow compares the PR branch against the base branch:
name: Visual Regression
on: [pull_request]
jobs:
happo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- run: npm ci
- name: Run Happo on current commit
env:
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
run: npx happo run
- name: Run Happo on base commit
env:
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
run: |
git checkout ${{ github.base_ref }}
npx happo run
- name: Compare
env:
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
run: |
npx happo compare ${{ github.sha }} ${{ github.event.pull_request.base.sha }}Happo integrates directly with GitHub's status checks — a green checkmark when no differences, a link to the review UI when changes are detected.
Using happo-ci
For a simpler setup, use happo-ci which handles the base/current comparison automatically:
npm install --save-dev happo-ci- name: Run Happo CI
env:
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
PREVIOUS_SHA: ${{ github.event.pull_request.base.sha }}
CURRENT_SHA: ${{ github.sha }}
CHANGE_URL: ${{ github.event.pull_request.html_url }}
run: npx happo-ciHandling Dynamic Content
Dynamic content (timestamps, random IDs, user-specific data) causes false positives. Use Happo's hide utility:
import { hide } from 'happo.js';
export const userCard = () => (
<UserCard
name="Test User"
joinDate={hide(<span>January 1, 2024</span>)}
avatar={hide(<Avatar src="/avatar.jpg" />)}
/>
);The hide wrapper replaces the element with an empty box of the same dimensions in screenshots, preserving layout while removing dynamic content.
Cross-Browser Comparison
After running, Happo's comparison UI shows:
- Diff between your branch and base — per browser
- Cross-browser diffs — Chrome vs Firefox, Chrome vs Safari
Cross-browser diffs help you spot platform-specific rendering issues before users report them. Common findings:
- Font rendering differences between Chrome and Safari
- Border-radius rendering on older Firefox versions
- Input styling differences between platforms
- Flexbox gap support differences
Reviewing and Approving Changes
When Happo finds differences:
- Click the comparison link in the PR status check
- Review each changed component across browsers
- Accept intentional changes (updates the baseline)
- Reject unintentional regressions (blocks merge)
Happo tracks acceptance per-component, per-browser — you can accept a change in Chrome while rejecting it in Firefox if only one browser has the right rendering.
Happo vs Percy vs Chromatic
| Feature | Happo | Percy | Chromatic |
|---|---|---|---|
| Multi-browser | Yes (Chrome/Firefox/Safari/Edge) | Chrome + Firefox | Chrome only |
| Storybook native | Plugin | Native | Native |
| Cross-browser diffs | Yes | Limited | No |
| Self-hosted option | No | No | No |
| Open source plan | Yes | No | Yes |
Happo's multi-browser support justifies it for projects where Safari or Firefox compatibility matters — design systems, consumer-facing applications, or accessibility-focused products.
Summary
Happo fills the cross-browser gap in visual regression testing:
- Simultaneous screenshots across Chrome, Firefox, Safari, and Edge
- Cross-browser diff detection beyond regression tracking
- Storybook integration through
happo-plugin-storybook - CI-native with GitHub status checks
- Review UI with per-component, per-browser approval workflow
For teams shipping components that need to look consistent across browsers, Happo catches the class of bugs that Chromium-only tools miss.