Visual regression tests for a converted Framer site with Playwright

Use Playwright screenshots to compare a converted Framer site with the live original, then keep later edits from breaking the design. Setup, config and tips.

Published
Reading time
5 min
Deframer cover: “Screenshot it. Catch drift.” Beside it, a list of 1440 px, 768 px and 390 px with 1440 px selected
On this page12 sections

The simplest way to test a converted Framer site visually is to use the live Framer site as the baseline. With Playwright, take screenshots of every route on the published site at three widths, save them as the expected images, then run the same tests against the converted project. Differences show up as image diffs. Once the converted site is accepted, re-baseline against it, and the same tests stop later edits from quietly breaking the design.

This guide uses a Next.js project converted by Deframer, but the tests only need URLs, so they work the same for TanStack Start, Astro, React Router and Vite. Deframer is an independent tool and is not affiliated with, endorsed or sponsored by Framer. It converts only sites you own or have permission to convert.

Why visual tests suit a converted site

A converted site is mostly design: layout, type, colour, spacing, breakpoints. Unit tests don't catch a heading that wraps differently on a phone or a section that lost its padding. Screenshots do. And unlike most projects, you start with a reference that's already correct: the published site. If live previews are on, Deframer's compare view gives a first look side by side; Playwright turns that one-off review into something repeatable.

Step 1: Install Playwright

In the project root:

sh
npm init playwright@latest

Pick TypeScript, put tests in tests/, and let it install a browser. For screenshot tests, one browser (Chromium) is enough to start; rendering differs slightly between browsers, so mixing them in one baseline makes noise.

Step 2: List the routes

The project's README.md has a routes table, URL to route file. Put the URLs in a file the tests read:

ts
// tests/routes.ts
export const routes = ["/", "/about", "/pricing", "/blog", "/blog/first-post", "/contact"];

Include at least one page of each kind: home, a static page, a CMS list and a CMS detail page, and the 404 page if the site designed one.

Step 3: Write the test

ts
// tests/visual.spec.ts
import { test, expect } from "@playwright/test";
import { routes } from "./routes";

const widths = [1440, 768, 390];

for (const path of routes) {
  for (const width of widths) {
    test(`${path} at ${width}px`, async ({ page }) => {
      await page.setViewportSize({ width, height: 900 });
      await page.emulateMedia({ reducedMotion: "reduce" });
      await page.goto(path, { waitUntil: "networkidle" });
      await page.evaluate(() => document.fonts.ready);
      await expect(page).toHaveScreenshot({
        fullPage: true,
        maxDiffPixelRatio: 0.01,
      });
    });
  }
}

What each line is for:

  • Three widths: about 1440, 768 and 390 pixels match the desktop, tablet and phone breakpoints most Framer sites use. Breakpoints are separate layouts in the converted project, so each needs its own check.
  • Reduced motion: appear effects and scroll animations make screenshots flaky. Emulating prefers-reduced-motion helps when the site honours it; toHaveScreenshot also disables CSS animations by default. If some elements still animate in, scroll the page to the bottom and back before the screenshot, or wait for a known element.
  • Fonts ready: a screenshot taken before web fonts load shows fallback fonts, the most common false diff.
  • maxDiffPixelRatio: a small tolerance absorbs anti-aliasing. Start at 1% and tighten if it hides real changes.

Step 4: Point the config at either site

Make the base URL come from the environment:

ts
// playwright.config.ts (excerpt)
import { defineConfig } from "@playwright/test";

const baseURL = process.env.BASE_URL ?? "http://localhost:3000";

export default defineConfig({
  testDir: "tests",
  use: { baseURL },
  webServer: process.env.BASE_URL
    ? undefined
    : { command: "npm run build && npm start", url: "http://localhost:3000", reuseExistingServer: true, timeout: 300_000 },
});

Without BASE_URL, Playwright builds and starts the converted project. With it, the tests run against any address, including the live site.

Step 5: Baseline from the live Framer site

sh
BASE_URL=https://example.com npx playwright test --update-snapshots

This writes the expected screenshots from the published Framer site. Then run against the converted project:

sh
npx playwright test
npx playwright show-report

The report shows each failing route with the expected image, the actual image and a diff. Expect some failures on the first run; this is the review you'd otherwise do by eye.

Step 6: Mask what shouldn't count

Some differences are real but irrelevant:

  • The "Made in Framer" badge on sites without a paid plan appears only on the original.
  • Dates, counters, embeds and carousels change between loads.
  • Third-party widgets (chat, cookie banners) load late or differently.

Mask them:

ts
await expect(page).toHaveScreenshot({
  fullPage: true,
  mask: [page.locator("iframe"), page.locator("[data-testid=clock]")],
});

Masked areas are painted over in both images. Prefer masks over raising the tolerance, which hides real changes too.

Step 7: Triage the differences

Group diffs by cause rather than fixing them one by one. Common ones, with fixes in fixing layout differences:

Diff

Likely cause

Text slightly wider everywhere

Fallback font in the screenshot, or a font not loaded

One breakpoint off, others fine

A layout value in that breakpoint's media query

Section shifted down by a fixed amount

Something above it changed height

Image cropped differently

Object-fit or aspect ratio

Elements missing on one side

Appear effect captured mid-animation

Note each accepted difference in the handoff or review notes; reviewing a converted project has a template.

Step 8: Switch the baseline to the converted site

Once the converted site is accepted, it's the new reference:

sh
npx playwright test --update-snapshots
git add tests
git commit -m "Visual baselines from the accepted conversion"

From now on, a failing test means your own change altered the design. That's the safety net that makes refactors possible, such as adding Tailwind or rewriting a component.

Step 9: Run it in CI

Screenshots depend on the operating system's font rendering, so generate baselines in the same environment CI uses. The usual approach is to run Playwright in its official Docker image both locally (for updating baselines) and in CI. A GitHub Actions job:

yaml
name: visual
on: pull_request
jobs:
  visual:
    runs-on: ubuntu-latest
    container: mcr.microsoft.com/playwright:v1.50.0-noble
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: report, path: playwright-report }

Match the image tag to the Playwright version in your package.json. With preview deployments, you can also run the tests against each preview URL by setting BASE_URL.

Keeping the suite useful

  • Few routes, three widths beats every route at one width. Add routes when a page type is new.
  • Update baselines on purpose. A pull request that changes the design updates its screenshots in the same commit, so reviewers see the images change.
  • Keep tolerance low. If tests are flaky, find the cause (fonts, animation, dynamic content) instead of loosening the threshold.

FAQ

Can I compare against the Framer site forever?

You can, but the live site and the code will diverge as soon as you change either. Use the live site to accept the conversion, then switch the baseline to the converted project.

Why are my first screenshots so different?

Usually fonts or animation. Wait for document.fonts.ready, emulate reduced motion and mask dynamic parts. What remains is worth looking at.

Do I need a paid service for visual testing?

No. Playwright's toHaveScreenshot stores images in your repository and compares them locally or in CI. Hosted services add review dashboards, which some teams like.

Does this work for the Vite or Astro export?

Yes. The tests only need URLs. Change the webServer command to the project's own: npm run build && npm run preview for Vite, npm run build && npm start for the others.

Does Deframer run these tests for me?

No. When live previews are on, its compare view shows the original and the converted site side by side at the same three widths for a first review. The Playwright suite is yours to keep in the project.

Keep reading

All posts