DEV Community

Cover image for Pixel-Perfect HTML to WordPress: How to Convert a Design Without Changing a Pixel (and Prove It)
Kerf
Kerf

Posted on Originally published at kerf.site

Pixel-Perfect HTML to WordPress: How to Convert a Design Without Changing a Pixel (and Prove It)

Almost every HTML-to-WordPress service and plugin says “pixel-perfect.” Very few show you a measurement. And most people who have moved a hand-made or AI-made design into WordPress know the feeling: it looks nearly right. The hero is two pixels taller, the cards wrap one breakpoint too early, the menu lost its hover underline, the footer font fell back to Arial.

“Nearly” is the problem. A design is a set of decisions, and every small drift is a decision someone else made. This article is about getting an HTML design onto WordPress without any drift at all—and about how to prove it, with numbers, instead of squinting at two browser tabs.

What “Pixel-Perfect” Should Mean

A useful definition is a testable one: at the same viewport, with the same content, the WordPress page renders the same pixels as the original HTML page. Not “looks the same to me”—the same pixels. That gives you two things you can check:

  1. The pixels: screenshot both pages at the same size and subtract one from the other. Anything left over is a difference.
  2. The cause: every rule in your stylesheets should still match the elements it was written for. If a selector stops matching, some part of the page is no longer styled as designed—even if you haven’t spotted it yet.

On our sample design, Copperline Studio (an invented studio’s site, made as plain HTML), Kerf’s import gives 134 of 134 CSS selectors matched, and the two screenshots at 1440×900 subtract to black: 0 of 1,296,000 pixels differ. You can watch that subtraction run live on kerf.site. The rest of this article explains why most conversions can’t get there, and how to check your own.

Where HTML-to-WordPress Conversions Drift

Copying the stylesheet across is the easy part. The design breaks because the HTML around it changes, or because the CSS arrives in a different order. These are the usual culprits.

1. Wrapper elements

Page builders and generic block converters wrap your markup in their own containers: a section becomes a row inside a column inside a widget. Every extra element breaks selectors that depend on structure—.hero > h1, .cards > article:nth-child(3), .grid > *—and puts a new box in the layout that CSS Grid and Flexbox now treat as the item instead of your element.

2. Renamed or normalised markup

A <nav> replaced by wp_nav_menu() outputs WordPress’s own ul.menu > li.menu-item list, and every rule your designer wrote for the original menu stops matching. A table becomes <figure class="wp-block-table">. A <button> becomes a link styled by the theme. Each change is small; together they are why the page “almost” matches.

3. Cascade order and WordPress’s own CSS

CSS is order-sensitive. If your reset.css, style.css and animations.css load in a different order, or WordPress’s block-library stylesheet adds margins to every block, rules resolve differently. The result is spacing that is off by a few pixels everywhere—the hardest kind of drift to diagnose by eye.

4. Rewritten asset paths

A stylesheet that says url(../images/hero.jpg) or url(../fonts/inter.woff2) only works if the folder structure around it survives. Move the CSS into a theme folder and the pictures under /uploads/2026/10/, and backgrounds vanish and fonts silently fall back.

5. Scripts that no longer find their elements

Sliders, menus, scroll reveals and counters usually look for an ID or class. Change the markup, or load a script before the library it depends on, and the page renders—but stuck in its “before JavaScript” state.

How Kerf Keeps the Design Identical

Kerf is a WordPress theme built around one rule: don’t rewrite the design, make it editable. It imports a ZIP of HTML, CSS, scripts and pictures—from ChatGPT, Claude, Gemini, Google Stitch or a designer—and answers each source of drift above:

  • No wrappers, same tags. Structural elements become Kerf blocks that output the exact tag they came from—<section>, <article>, <nav>, <table>, <button>—with the same classes. The DOM WordPress serves is the DOM your CSS was written for.
  • The menu keeps its markup. You rename, reorder and re-link the menu’s items in WordPress, and it still renders your design’s own <nav>.
  • Stylesheets and scripts in source order. Kerf registers them through WordPress’s dependency system in the order your HTML loaded them, and by default it strips the block-library layout margins that would otherwise fight your CSS on imported pages.
  • Folders kept as they were. Your css/, js/, fonts/ and images/ folders are staged with their hierarchy intact, so relative URLs inside stylesheets resolve without rewriting.
  • A check before, a report after. Before anything is written, Kerf counts the pages, stylesheets, scripts and pictures in your ZIP and flags files the HTML refers to that are missing. After the import, the fidelity report checks every selector in your stylesheets against the WordPress page.

Kerf import report showing 100% CSS selector match on Copperline Studio

The fidelity report on Copperline Studio: 134 of 134 CSS selectors still match after the import.

And because the words, pictures, links and menu become blocks, everything stays editable in WordPress’s block editor—with your real stylesheets loaded in the editor canvas, so what you edit looks like the live page.

Prove It Yourself: A Screenshot Diff in Ten Minutes

Don’t take any tool’s word for “pixel-perfect,” including ours. Here is how to measure any conversion.

1. Render both pages under identical conditions

  • Same viewport (for example 1440×900, then 390×844 for a phone).
  • Logged out, so WordPress’s admin bar isn’t drawn on top.
  • Fonts loaded before capturing (document.fonts.ready).
  • Motion settled: wait for scroll-reveal and entrance animations to finish on both pages.
  • Same content. Compare before you edit anything.

2. Capture with a headless browser

With Playwright (Node), for example:

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
for (const [name, url] of [['html', 'http://localhost:8080/index.html'], ['wp', 'https://your-site.test/']]) {
  await page.goto(url, { waitUntil: 'networkidle' });
  await page.evaluate(() => document.fonts.ready);
  await page.waitForTimeout(1500); // let entrance animations finish
  await page.screenshot({ path: name + '.png' });
}
await browser.close();
Enter fullscreen mode Exit fullscreen mode

3. Subtract one from the other

ImageMagick counts the differing pixels and draws them for you:

magick compare -metric AE html.png wp.png diff.png
# prints how many pixels differ: 0 means identical
Enter fullscreen mode Exit fullscreen mode

No ImageMagick? Put the two screenshots on top of each other in a page and set mix-blend-mode: difference on the top one. Identical pixels cancel to pure black; any drift glows. That is the subtraction kerf.site runs in your browser.

4. Audit the selectors

A screenshot only covers what is on screen at that size. To catch problems in states a single snapshot misses, check that the stylesheet selectors still match something on the converted page:

// Run in the browser console, on the original page and then on the WordPress page
const unmatched = [];
for (const sheet of document.styleSheets) {
  let rules; try { rules = sheet.cssRules; } catch { continue; } // skip cross-origin sheets
  for (const rule of rules) {
    if (!rule.selectorText) continue;
    const sel = rule.selectorText.replace(/::?(hover|focus-visible|focus-within|focus|active|visited|before|after)/g, '');
    try { if (sel.trim() && !document.querySelector(sel)) unmatched.push(rule.selectorText); } catch {}
  }
}
console.log(unmatched.length + ' selectors match nothing', unmatched);
Enter fullscreen mode Exit fullscreen mode

The selectors that match on the original page and not on WordPress are your drift. Kerf’s fidelity report runs this kind of check for you at the end of every import.

A Pixel-Perfect Checklist for AI-Made Designs

  • Export real HTML. HTML, CSS and JavaScript files, or a ZIP of them. Apps that need a build step—typical React projects—must be exported as static HTML first; Kerf doesn’t import them yet.
  • Keep assets local and relative. images/, css/, fonts/ inside the ZIP, referenced with relative paths.
  • Keep shared styles in a stylesheet rather than repeated inline <style> blocks, so every page shares them.
  • Compare before you edit. Run the screenshot diff right after the import, while the content is still identical.
  • Check phone width too. Most drift shows up at breakpoints.

Try It With Your Own Design

Pixel-perfect is a claim you should be able to test in minutes, not after a purchase. Try Kerf free in your browser: it opens a private WordPress with Kerf installed—no sign-up, nothing to install. Drop in your ZIP, read the check and the fidelity report, then screenshot both pages and subtract them.

The short version: A conversion drifts when the markup, the cascade order or the asset paths change. Kerf keeps all three as written, makes the content editable around them, and reports any selector that stops matching. On Copperline Studio: 134 of 134 selectors, 0 of 1,296,000 pixels different. Read the docs →


Originally published on kerf.site. Try Kerf free in your browser — a private WordPress with Kerf installed, no sign-up: https://kerf.site/demo/

Top comments (0)