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:
npm init playwright@latestPick 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:
// 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
// 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-motionhelps when the site honours it;toHaveScreenshotalso 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:
// 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
BASE_URL=https://example.com npx playwright test --update-snapshotsThis writes the expected screenshots from the published Framer site. Then run against the converted project:
npx playwright test
npx playwright show-reportThe 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:
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:
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:
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.
