Loki: Storybook Visual Regression Testing
Loki is a visual regression testing tool purpose-built for Storybook. It runs locally and in CI, captures screenshots of every story, compares them against stored references, and shows diffs when components change. Unlike cloud-hosted alternatives, Loki stores reference images in your repository and runs entirely offline.
Why Loki
Loki fills a specific niche: teams that want visual regression testing without external service dependencies or per-screenshot billing. References live in .loki/reference/ — committed to your repository, diffed in code review, and available without network access.
Tradeoffs vs hosted alternatives:
- Pro: fully offline, no API keys, no billing, references in version control
- Pro: fast — runs in the same process as your Storybook
- Con: no multi-browser support (Chromium only via Puppeteer/Chrome)
- Con: no hosted review UI — diffs are local image files
Installation
npm install --save-dev loki
npx loki initinit adds Loki configuration to package.json and creates .loki/ directory structure.
Configuration
Loki is configured in package.json:
{
"loki": {
"configurations": {
"chrome.laptop": {
"target": "chrome.app",
"width": 1366,
"height": 768
},
"chrome.iphone7": {
"target": "chrome.app",
"width": 375,
"height": 667
}
}
}
}Targets:
chrome.app— requires Chrome installed locally (fastest)chrome.docker— runs Chrome in Docker (consistent across machines)ios.simulator— requires macOS + Xcodeandroid.emulator— requires Android SDK
For CI and team consistency, use chrome.docker.
Docker Target Configuration
{
"loki": {
"configurations": {
"chrome.laptop": {
"target": "chrome.docker",
"width": 1366,
"height": 768
},
"chrome.mobile": {
"target": "chrome.docker",
"width": 375,
"height": 667
}
}
}
}With chrome.docker, Loki pulls and starts a Chrome container automatically. Requires Docker to be running.
Commands
# Start Storybook first
npm run storybook &
<span class="hljs-comment"># Capture reference screenshots (first run or after intentional changes)
npx loki update
<span class="hljs-comment"># Test against references (fails on any difference)
npx loki <span class="hljs-built_in">test
<span class="hljs-comment"># Approve current screenshots as new references
npx loki approveThe workflow:
update— creates.loki/reference/*.pngfiles- Commit references to git
testin CI — fails if any story differs from reference- After intentional changes:
approve, commit new references
Storybook Integration
Loki discovers stories from your Storybook configuration. It supports Storybook 6 and 7.
For Storybook 7, ensure storiesOf or CSF format stories are registered:
// src/components/Button/Button.stories.jsx
export default {
title: 'Components/Button',
component: Button,
};
export const Primary = {
args: { variant: 'primary', children: 'Click me' },
};
export const Secondary = {
args: { variant: 'secondary', children: 'Click me' },
};
export const Disabled = {
args: { variant: 'primary', disabled: true, children: 'Disabled' },
};Loki captures each named export as a separate screenshot.
Filtering Stories
Run Loki on a subset of stories with --storiesFilter:
# Test only Button stories
npx loki <span class="hljs-built_in">test --storiesFilter <span class="hljs-string">"Components/Button"
<span class="hljs-comment"># Test only components in the 'Forms' section
npx loki <span class="hljs-built_in">test --storiesFilter <span class="hljs-string">"Forms/"
<span class="hljs-comment"># Test by story name
npx loki <span class="hljs-built_in">test --storiesFilter <span class="hljs-string">"Primary"This is useful during development — test only the stories you're currently changing.
Handling Dynamic Content
Stories with dynamic content (random data, current timestamps, animated elements) produce unstable screenshots. Fix at the story level:
Disable animations in Storybook:
// .storybook/preview.js
export const decorators = [
(Story) => {
// Disable CSS animations for all stories
const style = document.createElement('style');
style.textContent = `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
}
`;
document.head.appendChild(style);
return <Story />;
},
];Use fixed data in stories:
// Don't use Math.random() or Date.now() in stories
export const UserCard = {
args: {
user: {
name: 'Alice Johnson',
joinDate: '2024-01-15', // fixed, not new Date()
avatar: '/test-avatar.png',
},
},
};Mock timers in stories with Storybook decorators:
// .storybook/preview.js
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
export const decorators = [
(Story, context) => {
if (context.parameters.mockDate) {
// Use @sinonjs/fake-timers or similar
}
return <Story />;
},
];Configuration: Thresholds and Diffing
Add threshold configuration for antialiasing tolerance:
{
"loki": {
"diffingEngine": "looks-same",
"configurations": {
"chrome.laptop": {
"target": "chrome.docker",
"width": 1366,
"height": 768,
"tolerance": 2.3,
"antialiasingTolerance": 3.5
}
}
}
}tolerance uses looks-same antialiasing-aware comparison. Increase it to reduce false positives from font rendering differences.
Viewing Diffs
When loki test fails, diffs are written to .loki/difference/. Each diff image shows:
- Left: reference (what it looked like before)
- Right: current (what it looks like now)
- Highlighted: changed pixels
# Open diffs on macOS
open .loki/difference/*.png
<span class="hljs-comment"># Or list what changed
<span class="hljs-built_in">ls .loki/difference/For a more integrated experience, use the Loki Storybook addon that shows pass/fail status alongside each story in the Storybook UI:
npm install --save-dev @loki/storybook-addon// .storybook/main.js
module.exports = {
addons: ['@loki/storybook-addon'],
};CI Integration
name: Visual Regression
on: [pull_request]
jobs:
loki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- run: npm ci
- name: Build Storybook
run: npm run build-storybook -- --quiet
- name: Run Loki visual regression tests
run: npx loki test --requireReference --reactUri file:./storybook-static
- name: Upload diffs on failure
if: failure()
uses: actions/upload-artifact@v3
with:
name: loki-diffs
path: .loki/difference/The --reactUri file:./storybook-static flag runs against a built Storybook (no server needed), which is faster and more reliable in CI than starting a dev server.
--requireReference fails if any story has no reference screenshot — catches new components added without updating references.
Updating References in CI
When a PR intentionally changes component appearance:
# Run locally
npx loki update
<span class="hljs-comment"># Commit the updated references
git add .loki/reference/
git commit -m <span class="hljs-string">"chore: update loki visual references for new button design"
git pushReferences are regular PNG files in .loki/reference/ — they show in code review diffs. Reviewers can see exactly which components changed visually.
Excluding Stories
Skip specific stories from Loki testing:
// In the story file
export const AnimatedComponent = {
args: { /* ... */ },
parameters: {
loki: { skip: true },
},
};Or configure globally:
{
"loki": {
"skipStories": ["Components/Loading/Spinner", "Experimental/*"]
}
}Performance
For large Storybook projects (100+ stories), Loki can be slow. Speed up:
{
"loki": {
"chromeFlags": "--disable-dev-shm-usage --no-sandbox",
"configurations": {
"chrome.laptop": {
"target": "chrome.docker",
"concurrency": 4
}
}
}
}Parallel screenshot capture with concurrency: 4 reduces total time by ~4x for large story sets.
Loki vs Percy vs Chromatic
| Feature | Loki | Percy | Chromatic |
|---|---|---|---|
| Self-hosted | Yes | No | No |
| References in git | Yes | No | No |
| Multi-browser | No | Yes | No |
| Review UI | Local files | Hosted | Hosted |
| Storybook native | Yes | Yes | Yes |
| Offline capable | Yes | No | No |
| Price | Free | Usage-based | Usage-based |
Loki is the right choice for teams that:
- Want references version-controlled alongside component code
- Can't use external services (security constraints, air-gapped environments)
- Don't need multi-browser comparison
- Want a zero-dependency, zero-cost solution
Summary
Loki provides Storybook visual regression testing with minimal setup:
- References stored in
.loki/reference/alongside code update→test→approveworkflow- Docker target for consistent cross-machine screenshots
- Story-level exclusion and threshold configuration
- CI-native with artifact upload for diff review
For teams already invested in Storybook and wanting visual regressions caught in PR review, Loki adds the visual layer without requiring external accounts or services.