Screenery

Compare / Lost Pixel

Moving from Lost Pixel to Playwright and Screenery

The Lost Pixel team announced on 22 April 2026 that they are joining Figma and sunsetting the product. The repository was archived the same day, and the announcement names no migration path. This guide moves each kind of Lost Pixel shot to Playwright and publishes the result with Screenery.

Where Lost Pixel stands, checked October 2026

What Lost Pixel did well

Lost Pixel tested several kinds of UI in one run, configured in lostpixel.config.ts:

In open-source mode it compared .lostpixel/current with a committed .lostpixel/baseline. The hosted Platform added a review screen and approvals. It was free for open source and easy to start with, and it is a loss.

Side by side

Lost PixelPlaywright + Screenery
Stories Built in for Storybook, Ladle, Histoire No Storybook integration. A Playwright test can visit each story
Pages pageShots in the config A Playwright test
Custom shots A folder of images A folder of images, pushed as it is
Baseline .lostpixel/baseline, or the Platform A committed folder, or the version each push replaces on Screenery
CI auth Platform API key GitHub OIDC, no stored secret. Other CI: SCREENERY_TOKEN
Published URLs None @latest follows the default branch
Status Archived April 2026 Active

Step 1: publish what you have

If you still run the Lost Pixel engine in open-source mode, its output is a folder of PNGs. Check whether Screenery accepts the names with a dry run, which contacts nobody:

npx screenery push ./.lostpixel/current --project acme/dashboard --dry-run \
  --commit-timestamp 2026-10-01T00:00:00Z

Story shots are named in kebab case, such as example-button--primary, and usually pass. Shots with breakpoints, or with a browser name, can get a label such as __[w320px]. Brackets are not allowed in Screenery names, so the dry run names those files and a real push would refuse them. Capital letters in a page shot's name fail too. See Names.

If the dry run passes, add the Action after the Lost Pixel step. Lost Pixel stays your gate for now, and Screenery publishes what it accepted:

permissions:
  id-token: write
  contents: read
  pull-requests: write
steps:
  # … check out, install, build, run Lost Pixel …
  - uses: screenery/push@v1
    with:
      project: acme/dashboard
      path: .lostpixel/current

This is a bridge. The steps below remove the archived engine.

Step 2: custom shots

customShots pointed Lost Pixel at a folder your tests already filled. Point Screenery at the same folder and drop that block from the config:

- uses: screenery/push@v1
  with:
    project: acme/dashboard
    path: screenshots

If those are Playwright goldens from toHaveScreenshot, read Playwright goldens and Argos folders first.

Step 3: page shots

A pageShots list becomes a loop in a Playwright test. Set baseURL in playwright.config.ts to the baseUrl you gave Lost Pixel.

// tests/pages.spec.ts
import { test } from '@playwright/test';

const pages = [
  { path: '/', name: 'home' },
  { path: '/pricing', name: 'pricing' },
];

for (const { path, name } of pages) {
  for (const width of [320, 1280]) {
    test(`${name} at ${width}px`, async ({ page }) => {
      await page.setViewportSize({ width, height: 800 });
      await page.goto(path);
      await page.screenshot({ path: `test-results/${name}/w${width}.png`, fullPage: true });
    });
  }
}

Lost Pixel's breakpoints become the width loop, and the width goes last in the path, so home/w320 and home/w1280 sit together as one screen. waitForSelector becomes await page.locator(selector).waitFor(), and mask becomes the mask option of page.screenshot().

Step 4: stories

Screenery has no Storybook, Ladle or Histoire integration. If stories are most of what you test, look at Chromatic or Argos, which both have one.

If you only need a few stories published, plain Playwright can visit them. A built Storybook lists its stories in index.json, and each one renders alone at iframe.html?id=…:

// tests/stories.spec.ts
import { readFileSync } from 'node:fs';
import { test } from '@playwright/test';

const index = JSON.parse(readFileSync('storybook-static/index.json', 'utf8'));
const stories = Object.values(index.entries).filter((entry: any) => entry.type === 'story');

for (const story of stories as Array<{ id: string }>) {
  test(story.id, async ({ page }) => {
    await page.goto(`http://localhost:6006/iframe.html?id=${story.id}&viewMode=story`);
    await page.screenshot({ path: `test-results/stories/${story.id}.png` });
  });
}

Serve storybook-static on port 6006 first, for example with Playwright's webServer option. Story ids are already lowercase, so the names pass.

Step 5: the gate

Lost Pixel's failOnDifference becomes Screenery's diff gate. Create a baseline from your current screenshots, on the same image CI uses, and commit it:

npx playwright test
npx screenery diff ./test-results --baseline ./screenery-baseline --update
git add screenery-baseline

Then point the Action at it. A visual change fails the job, and nothing is published:

- uses: screenery/push@v1
  with:
    project: acme/dashboard
    path: test-results
    diff-baseline: ./screenery-baseline

If you would rather not commit images, set failAbove in screenery.config.json. Each push is then graded against the version it replaces on Screenery. See Visual testing. When everything runs, remove lost-pixel and lostpixel.config.ts.

You also gain something Lost Pixel never had: each screenshot's @latest URL follows your default branch, so it can go straight into a README or docs page. See Getting started.

Other destinations

Screenery is not the only answer. Argos is open source and close to Lost Pixel's model, with a review app. Chromatic is strongest for Storybook. Percy renders in several browsers. See all comparisons.

Sources

Checked October 2026.