Migrate from Loki
Move Storybook visual tests from Loki to Snapvisor - how loki test, update and approve, the .loki reference folder, chromeSelector, configurations, tolerance and skipped stories translate.
This guide moves Storybook visual tests from Loki to Snapvisor. Your stories and your CI stay. What changes is how the screenshots are taken, and where baselines and review live. The Storybook setup itself, including the Test Runner route and the CI wiring, is the same as in Migrate from Chromatic, so this guide links there instead of repeating it.
It was written against loki 0.35.1 and @snapvisor/storybook 7.0.0.
The one change that matters
Loki renders each story, writes the screenshots to .loki/current, compares them with the images in .loki/reference, and writes diffs to .loki/difference. loki update creates the reference images, and loki approve replaces them with the latest ones. Teams commit .loki/reference to Git.
Snapvisor stores baselines server-side, per reference branch. Your Storybook test run captures a screenshot of each story, your pipeline uploads them, and Snapvisor compares them with the approved screenshots for the branch. You approve changes in the review UI, and they become the new baseline for that branch. See Baseline builds.
Your reference images do not carry over, and there is no import path. Your first Snapvisor build on your base branch becomes the baseline. Plan the switch for a moment when your base branch is visually correct.
Concept mapping
| Loki | Snapvisor equivalent |
|---|---|
loki test | Your Storybook test run, with @snapvisor/storybook registered. Every story the run visits is captured. |
loki update | The first build on your base branch. |
loki approve | Approve the change in the review UI. See Review workflow. |
.loki/reference in Git | Nothing in your repository. Baselines live on the server. |
.loki/current and .loki/difference | The build in the Snapvisor app shows the before, the after and the diff. |
configurations with width and height or a preset | Storybook viewports and modes. See Viewports and modes. |
chromeSelector (default #root > *, #storybook-root > *) | Fit to content, which is on by default. See What gets captured. |
chromeTolerance or a diffing engine's options | threshold. See Tolerances. |
parameters: { loki: { skip: true } } | Snapvisor screenshots every story your test run visits. See Skipped stories. |
--requireReference (on by default when a CI environment is detected) | Not needed. A build with no baseline yet is simply the first one. |
--reactUri pointing at a built Storybook | Your Storybook test run starts and renders the stories. |
chrome.docker target for stable rendering | Not required for baselines, since they are not machine files. Keep CI in one consistent environment. |
| Asynchronous stories signalling when they are ready | Snapvisor waits for fonts and images to settle before capturing. For anything slower, wait in the story's play function. |
A failed loki test in the CI job | A change flagged in the Snapvisor review and on the pull request status check. |
Swap the capture
Install the SDK:
npm install --save-dev @snapvisor/storybookThe recommended route is the Storybook Vitest addon. Register the plugin in your Vitest config:
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { argosVitestPlugin } from "@snapvisor/storybook/vitest-plugin";
export default defineConfig({
plugins: [
argosVitestPlugin({
// Set to true to upload; nothing is uploaded when this is omitted.
uploadToArgos: process.env.CI === "true",
}),
],
});A screenshot is captured for every story automatically, so there is no list of stories to maintain. To capture extra screenshots at different steps of an interaction, call argosScreenshot from @snapvisor/storybook/vitest inside a story's play function:
// Button.stories.tsx
import { argosScreenshot } from "@snapvisor/storybook/vitest";
export const Example: Story = {
play: async (ctx) => {
await argosScreenshot(ctx, "example");
},
};If you run stories with the Storybook Test Runner instead, import argosScreenshot from @snapvisor/storybook/test-runner and call it from the postVisit hook. The setup is shown in the Storybook section of the Chromatic guide.
The screenshot name for each story is its story ID, so it stays stable as long as the story ID does.
What gets captured
Loki crops each screenshot to chromeSelector, which by default is the story root. Snapvisor does something similar by default: it fits the capture to the story's content, with 16 pixels of padding and a zoom of 2. To change that for a story or for all stories, set the argos parameter:
// .storybook/preview.ts
export const parameters = {
argos: {
// Capture the whole page instead of fitting to the content.
fitToContent: false,
},
};fitToContent also accepts { padding, zoom } to change either value. Loki's chromeSelector is a CSS selector for the area to keep, and fitToContent is a different mechanism, so check a few screenshots after the switch rather than assuming the crops match.
Snapvisor also stabilizes the page before capturing. It waits for web fonts and images to settle, hides carets and scrollbars, and normalizes font anti-aliasing. In Storybook captures the aria-busy wait is off by default; passing your own stabilize object replaces that default, so include waitForAriaBusy: false in it to keep the wait off.
Viewports and modes
A Loki configuration such as chrome.iphone7 or chrome.laptop renders every story at one size. In Snapvisor, set the size with Storybook's own viewport parameter, and capture the same story in several variants with modes:
// .storybook/preview.ts
export const parameters = {
viewport: {
viewports: {
phone: { name: "Phone", styles: { width: "375px", height: "667px" } },
},
},
argos: {
modes: {
phone: { viewport: "phone" },
wide: { viewport: 1280 },
dark: { theme: "dark" },
},
},
};Each mode produces its own screenshot, named after the story ID and the mode, such as button--example mode-[phone]. A mode is a set of Storybook globals, so it can change the viewport, a theme or any other global your stories read. A viewport global is either a width in pixels (the height is 720) or the name of a size defined under the viewport parameter with a pixel width and height in its styles. To switch a mode off for a story, set disabled: true on it in that story's argos parameter. Without modes, a story is captured at its defaultViewport from the viewport parameter when it has one.
Skipped stories
Loki lets you opt a story out with parameters: { loki: { skip: true } }, or exclude stories with --skipStories and --storiesFilter. Snapvisor captures every story your test run visits. To leave a story out, keep it out of the test run, using the filtering your Storybook test runner provides. Rather than skipping a story because it is flaky, try the stabilization above first, or fix what makes it change.
Tolerances
Loki compares with pixelmatch by default, and chromeTolerance defaults to 0. You can switch the diffing engine to looks-same or gm and pass options to it.
Snapvisor has one setting, threshold, a sensitivity between 0 and 1 that defaults to 0.5. Higher means less sensitive. It is a different kind of setting from Loki's tolerance and engine options, so do not copy a number across. Set it for all stories on the plugin:
argosVitestPlugin({ threshold: 0.7 });Start from the default and adjust only when a specific screenshot is noisy. If you raised the tolerance to absorb font or anti-aliasing differences between machines, remove it and try the default first. A change is flagged for review, not failed, so a small difference is a decision for a reviewer and not a red build.
The CI change
Before, the job needed a running or built Storybook and ran Loki, and a changed story failed the job:
- run: npm ci
- run: npm run build-storybook
- run: npx loki --requireReference --reactUri file:./storybook-staticAfter, the job runs your Storybook test run with the project token. A changed screenshot no longer fails the job:
- run: npm ci
- run: npm run test-storybook # your Storybook Vitest or Test Runner command
env:
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}Copy the project token from your project's settings in the app, and store it as a CI secret, never in the repository. See Access tokens. On GitHub Actions you can drop the secret with tokenless authentication. More workflows, including merge queues and fork pull requests, are in GitHub Actions recipes.
Run both side by side
You do not have to switch in one pull request. Keep loki test running and add the Snapvisor plugin to your Storybook test run:
- Add the plugin, and merge it to your base branch so the first Snapvisor build becomes the baseline.
- Keep Loki as your gate, and the Snapvisor check optional, while you compare results.
- When the Snapvisor builds match your expectations on a few pull requests, make the Snapvisor check required, then remove Loki.
Make the Snapvisor check the gate
The upload finishes once the build has been created, whether or not it contains changes. A failed upload (bad token, unresolvable commit, network) is an error. The review gate is the commit status check Snapvisor posts to the pull request, which asks for review until someone approves or rejects the changes.
What you can delete
Once builds are landing in Snapvisor and the check is required:
- The
lokidependency, thelokikey inpackage.json(or your Loki config file), and theloki test,loki updateandloki approvescripts. - The
.loki/referencefolder. Remove it from Git LFS too if you stored references there. - The
.gitignorerules for.loki/currentand.loki/difference. loki: { skip: true }parameters and anychromeSelectordecorators that existed only to crop screenshots.- The Docker image or job that existed only to render Loki references on one machine, and any job that builds Storybook only for Loki.
Your pull requests stop carrying binary image changes. Reviewers see the before and after in the review UI instead.
First-build checklist
After the first Snapvisor build on your base branch:
- Open the build and confirm it has a screenshot for each story, and each mode or viewport you configured.
- Look through the screenshots for fonts, images and animations that did not settle. Fix those first, before any threshold change.
- Run the same build twice on one commit and check that nothing shows as changed.
- Open a pull request with one small visual change. Confirm the Snapvisor status check appears, asks for review, and clears after you approve.
- Make the Snapvisor check required in branch protection.
Frequently asked questions
- Do I have to stop using Storybook?
- No. Snapvisor captures through your Storybook test run, so your stories stay as they are. Loki needs a running Storybook to render them. Snapvisor runs through the Storybook Vitest addon or the Storybook Test Runner.
- Can I import my .loki/reference images as baselines?
- No. There is no import path. Your first Snapvisor build on your base branch becomes the new baseline: it has nothing to compare against, so it is approved automatically. Run it when your base branch is visually correct.
- What replaces loki approve?
- Approving in the Snapvisor web app. You review the changes in a build and approve or reject them there. Nothing is copied between folders and no image files are committed.
- Can I run Loki and Snapvisor side by side while I switch?
- Yes. They are independent. Keep loki test running, add the Snapvisor plugin to your Storybook test run, and remove Loki once the Snapvisor check is your gate.
Migrate from Cypress image snapshot plugins
Move a Cypress suite from cypress-image-snapshot or cypress-visual-regression to Snapvisor - how cy.matchImageSnapshot and cy.compareSnapshot, committed snapshot folders, thresholds and update flags translate to cy.argosScreenshot.
Baseline builds
The baseline is the last approved build on your base branch. Learn how Snapvisor picks the reference build and resolves the base branch for every comparison.