Skip to content

Instantly share code, notes, and snippets.

@dsheiko
Created July 1, 2026 15:58
Show Gist options
  • Select an option

  • Save dsheiko/5f5f3263b96992f2533e07bfd2f4575e to your computer and use it in GitHub Desktop.

Select an option

Save dsheiko/5f5f3263b96992f2533e07bfd2f4575e to your computer and use it in GitHub Desktop.
CSS Style Sync — Claude Code Skill for Legacy-vs-New Computed Style Diffing (.claude/skills/css-style-sync/SKILL.md)
name css-style-sync
description Use this skill whenever a user wants to compare CSS computed styles between two webapps (e.g. a legacy app and its replacement, a staging vs. production build, or any "old vs. new" pair) and fix style discrepancies. Triggers when the user mentions comparing styles between two URLs, fixing visual regressions after a migration or rewrite, or syncing CSS from an old site to a new one. Also triggers for phrases like "same styles as legacy", "match the original design", "computed style diff", "CSS doesn't match", or any request to extract and compare computed styles between two pages/selectors. Always use this skill — it runs Playwright automatically to extract styles without the user needing to open DevTools. This skill is app-agnostic: it never assumes a specific framework, port, or URL, and always interviews the user for both targets before running.

CSS Style Sync Skill

Compares computed CSS between two webapps ("legacy"/source and "new"/target) using Playwright, then generates a CSS fix. Works with any framework or stack on either side (static HTML, Next.js, React, Vue, WordPress, etc.) — the skill never hardcodes a project's URLs, ports, or CSS approach.

Scripts (in scripts/):

  • extract_styles.py — extracts computed styles from one URL+selector via Playwright → JSON
  • compare_styles.py — runs both extractions, diffs them, and prints the CSS fix

Step 1 – Interview the User for Inputs

This skill has no built-in notion of which app is "legacy" and which is "new," and no default URLs or ports. Every run starts by asking the user directly (use the question/interview tool available in your environment, e.g. AskUserQuestion) for:

  1. Legacy app URL — the source of truth (e.g. http://localhost:3000/ or https://old.example.com/)
  2. Legacy CSS selector (e.g. #sidebar > div.sidebar-header.border-bottom > div)
  3. New app URL — the target being fixed (e.g. http://localhost:3003/ or https://staging.example.com/)
  4. New app CSS selector (e.g. #sidebar > div.sidebar-header.border-bottom > a)

Do not infer these values from project config, CLAUDE.md, or prior conversation defaults — always confirm them with the user, since the two URLs and selectors change on every project this skill is used in.

Optional: --wait-for <selector> to wait for a lazy-rendered element before extracting.


Step 2 – Verify HTML Structure Match (Gate)

Before comparing styles, confirm that both selectors point to structurally equivalent elements. This prevents applying styles meant for one element type onto a different one.

Run this inline Playwright script:

from playwright.sync_api import sync_playwright
import json

def get_structure(url, selector, wait_for=None):
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto(url, wait_until="networkidle")
        if wait_for:
            page.wait_for_selector(wait_for)
        el = page.query_selector(selector)
        if not el:
            return None
        tag = el.evaluate("el => el.tagName.toLowerCase()")
        children = el.evaluate(
            "el => Array.from(el.children).map(c => c.tagName.toLowerCase())"
        )
        browser.close()
        return {"tag": tag, "children": children, "count": len(children)}

legacy = get_structure("LEGACY_URL", "LEGACY_SELECTOR")
new    = get_structure("NEW_URL",    "NEW_SELECTOR")

print("Legacy:", json.dumps(legacy))
print("New:   ", json.dumps(new))

tag_match      = legacy["tag"] == new["tag"]
child_tags_match = sorted(legacy["children"]) == sorted(new["children"])
print("Tag match:", tag_match)
print("Children match:", child_tags_match)

Decision rules:

Result Action
Tag matches AND children match Proceed to Step 3
Tag matches, children differ Warn the user, list mismatched children, ask them to confirm before proceeding
Tag does not match Stop. The selectors point to different element types. Ask the user to provide a corrected target selector before continuing.
Either selector returns null Stop. Element not found. Ask the user to check the selector or add --wait-for.

Do not proceed to style comparison until the gate passes (or the user explicitly confirms they want to continue despite a child mismatch).


Step 3 – Run Playwright Comparison

Copy the scripts to a writable location and run:

cp -r /path/to/skill/scripts /tmp/css-sync-scripts

python3 /tmp/css-sync-scripts/compare_styles.py \
  --legacy-url "LEGACY_URL" \
  --legacy-sel "LEGACY_SELECTOR" \
  --new-url "NEW_URL" \
  --new-sel "NEW_SELECTOR" \
  --json-out /tmp/css-diff.json

The script outputs a colour-coded diff table and a CSS fix block directly in the terminal.

Network constraint (important)

Claude's sandbox can typically only reach localhost and a small allowlist of domains. This means:

  • ✅ URLs on localhost (either app, on whatever port the user gave in Step 1) — usually work automatically
  • ⚠️ Any external/public URL — likely blocked by egress policy

When either URL is external and unreachable from the sandbox: run the comparison script locally on the user's machine instead. Provide the exact command for them to run, filled in with the URLs and selectors they gave in Step 1. The script has no dependencies beyond playwright (pip install playwright && playwright install chromium).

Alternatively: run the legacy extraction locally, save legacy_styles.json, then pass it to a modified call that loads from file instead of URL (see extract_styles.py --out flag).


Step 4 – Parse and Present the Diff

After running, read /tmp/css-diff.json and present:

  1. A summary: "X differences found (Y high impact, Z medium)"
  2. The colour-coded diff table from script output
  3. The ready-to-paste CSS fix block

Highlight high-impact differences (font, color, spacing) prominently.


Step 5 – Generate CSS Fix

The script outputs the fix automatically. Present it clearly and ask the user where to apply it, since the right place depends on the new app's stack:

  • A global stylesheet (e.g. globals.css, main.scss, a site-wide CSS file)
  • A component-scoped stylesheet (CSS module, scoped <style>, SCSS partial)
  • A utility-first setup (e.g. Tailwind @layer components)
  • CSS-in-JS (styled-components, emotion, inline style objects)

If unsure which applies, ask the user rather than guessing the new app's styling convention.


Step 6 – Verify

After the user applies the fix:

python3 /tmp/css-sync-scripts/extract_styles.py \
  "NEW_URL" \
  "NEW_SELECTOR" \
  --out /tmp/new_styles_after.json

Then re-diff against /tmp/legacy_styles.json. Confirm high-impact diffs are resolved.


Tips

  • If element not found: the page may not have rendered yet. Add --wait-for "SELECTOR" matching a stable element on the page.
  • For client-rendered SPAs (React, Vue, Next.js, etc.), always use --wait-for with a selector that confirms the route has hydrated.
  • font-family computed values include full fallback stacks — compare the first font only.
  • Computed rgb() vs rgba() — alpha differences are always flagged as high impact.
  • For utility-first frameworks like Tailwind, box-sizing, margin, and line-height resets from preflight are the most common culprits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment