| 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. |
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 → JSONcompare_styles.py— runs both extractions, diffs them, and prints the CSS fix
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:
- Legacy app URL — the source of truth (e.g.
http://localhost:3000/orhttps://old.example.com/) - Legacy CSS selector (e.g.
#sidebar > div.sidebar-header.border-bottom > div) - New app URL — the target being fixed (e.g.
http://localhost:3003/orhttps://staging.example.com/) - 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.
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).
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.jsonThe script outputs a colour-coded diff table and a CSS fix block directly in the terminal.
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).
After running, read /tmp/css-diff.json and present:
- A summary: "X differences found (Y high impact, Z medium)"
- The colour-coded diff table from script output
- The ready-to-paste CSS fix block
Highlight high-impact differences (font, color, spacing) prominently.
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.
After the user applies the fix:
python3 /tmp/css-sync-scripts/extract_styles.py \
"NEW_URL" \
"NEW_SELECTOR" \
--out /tmp/new_styles_after.jsonThen re-diff against /tmp/legacy_styles.json. Confirm high-impact diffs are resolved.
- 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-forwith a selector that confirms the route has hydrated. font-familycomputed values include full fallback stacks — compare the first font only.- Computed
rgb()vsrgba()— alpha differences are always flagged as high impact. - For utility-first frameworks like Tailwind,
box-sizing,margin, andline-heightresets from preflight are the most common culprits.