| name | computed-styles |
| description | Extract and compare computed CSS styles between a baseline URL and a dev/Storybook URL using Playwright MCP evaluate calls. Use when visual differences have been identified and you need to find the exact CSS property mismatches to fix. Pairs with the visual-diff skill for a complete visual QA workflow. |
Skill: Computed Style Comparison
Use Playwright MCP's evaluate to extract getComputedStyle() values from a baseline page and a current page, compare them property-by-property, and fix the differences with Tailwind classes.
When to Use
- After the
visual-diff skill has identified pixel differences between two pages
- When you need exact CSS property values to guide Tailwind class changes
- When visual comparison alone is ambiguous (e.g., is the font 20px or 24px?)
- When fixing a component to match production styling
When NOT to Use
- For initial discovery of visual differences (use
visual-diff first)
- When the pages have fundamentally different HTML structures
- For layout issues caused by content differences, not style differences
Prerequisites
- Playwright MCP is configured in
.vscode/mcp.json
- Both URLs are accessible (baseline and current)
- You know which elements to compare (from visual-diff results or user input)
Key Concepts
Accessing Storybook Content
Storybook renders stories inside an iframe. You must access elements through it:
const iframe = document.querySelector('#storybook-preview-iframe');
const doc = iframe.contentDocument;
const cs = doc.defaultView.getComputedStyle.bind(doc.defaultView);
const element = doc.querySelector('nav');
const styles = cs(element);
Accessing Regular Pages
For production or dev server pages, access elements directly:
const cs = window.getComputedStyle.bind(window);
const element = document.querySelector('nav');
const styles = cs(element);
Tailwind v4 Theme Gotcha
This project uses Tailwind v4 with a custom theme in src/styles/bitovi-theme.css. Standard utility classes may not map to expected values. For example, text-xl in this project is 1.5rem (24px), not the standard 20px. Always verify with getComputedStyle() after applying Tailwind classes, and use arbitrary values like text-[20px] when standard classes don't produce the right output.
CSS Specificity in Tailwind v4
Tailwind v4 uses CSS layers, so class order in the HTML doesn't determine specificity the way it did in v3. If you apply both lg:text-teal-950 and lg:text-teal-600 to the same element, whichever comes later in the generated CSS wins, not whichever is last in the class attribute. To conditionally apply colors, use ternary expressions:
className={`lg:text-teal-950 ${isActive ? 'lg:text-teal-600' : ''}`}
className={isActive ? 'lg:text-teal-600' : 'lg:text-teal-950'}
Workflow
Step 1: Plan which elements and properties to extract
Based on the visual-diff results, identify the regions with differences and map them to DOM elements. Build a list of element/property pairs to extract.
Common element targets:
| Region | Typical selector | Key properties |
|---|
| Nav container | nav, nav parent | height, padding, maxWidth, boxShadow, backgroundColor |
| Logo | nav img, nav svg | width, height |
| Nav items | nav ul > li | fontSize, fontWeight, color, lineHeight, margin, fontFamily |
| CTA button | button with CTA text | height, padding, borderRadius, backgroundColor, fontSize |
| Dropdown panel | [id^="dropdown-"] | backgroundColor, borderRadius, overflow, maxWidth |
| Card / list items | li, .card | margin, padding, fontSize, fontWeight, color, lineHeight |
| Headings | h1–h6, [class*="uppercase"] | fontSize, fontWeight, color, letterSpacing, textTransform |
| Links | a | color, fontSize, fontWeight, textDecoration |
Common properties to compare:
fontSize, fontWeight, fontFamily, lineHeight, letterSpacing, textTransform,
color, backgroundColor, borderRadius, boxShadow,
width, height, maxWidth, minHeight,
padding, margin, gap,
display, alignItems, justifyContent, flexDirection,
overflow, opacity
Step 2: Extract styles from the baseline
Navigate to the baseline URL and use mcp_playwright_browser_evaluate to extract all target properties in a single call. Return a structured object.
Template for production pages:
() => {
const cs = window.getComputedStyle.bind(window);
const nav = document.querySelector('nav');
return {
nav: {
height: cs(nav).height,
padding: cs(nav).padding,
maxWidth: cs(nav).maxWidth,
},
};
}
Tips for finding elements on production pages:
- Search by text content with TreeWalker or
querySelectorAll + .textContent filtering
- Search by class name fragments:
[class*="nav-icon"]
- Filter multiple matches by visibility:
getComputedStyle(el).display !== 'none'
- Walk up from a known text node:
element.closest('li'), .parentElement
Step 3: Extract styles from the current page
Navigate to the current URL and run the same extraction. For Storybook, use the iframe access pattern.
Template for Storybook:
() => {
const iframe = document.querySelector('#storybook-preview-iframe');
const doc = iframe.contentDocument;
const cs = doc.defaultView.getComputedStyle.bind(doc.defaultView);
const nav = doc.querySelector('nav');
return {
nav: {
height: cs(nav).height,
padding: cs(nav).padding,
maxWidth: cs(nav).maxWidth,
},
};
}
Step 4: Compare and build a diff table
Compare every extracted value. Build a table like:
| Element | Property | Baseline | Current | Match |
|---|
| nav | height | 80px | 60px | NO |
| nav | padding | 0px 128px | 0px 32px | NO |
| nav item | fontSize | 20px | 24px | NO |
| nav item | fontWeight | 600 | 600 | YES |
Focus on the mismatches.
Step 5: Fix with Tailwind classes
For each mismatch, determine the correct Tailwind class. Prefer standard utilities; use arbitrary values when the theme doesn't have an exact match.
| Desired value | Tailwind approach |
|---|
| Standard spacing (4px, 8px, 16px...) | p-1, p-2, p-4 etc. |
| Non-standard spacing | p-[13px], m-[38px] |
| Standard colors in theme | text-teal-600, bg-teal-950 |
| Box shadows | shadow-[0_0_24px_-4px_rgb(191,197,198)] |
| Font sizes matching theme | Check bitovi-theme.css @theme block first |
| Font sizes not in theme | text-[20px] |
Step 6: Verify the fix
After applying changes, re-extract computed styles from the current page and confirm the values now match the baseline. This is critical because:
- Tailwind theme overrides can produce unexpected values
- CSS specificity conflicts can prevent classes from taking effect
- Parent element styles can cascade and override child styles
Step 7: Repeat for remaining differences
Work through the diff table systematically. After fixing a batch of related properties (e.g., all nav container styles), re-extract and verify before moving to the next batch.
Comprehensive Extraction Pattern
For efficiency, extract all properties in a single evaluate call rather than making many small calls. Here's a pattern for extracting a full nav component:
() => {
const cs = window.getComputedStyle.bind(window);
const props = (el) => !el ? null : ({
fontSize: cs(el).fontSize,
fontWeight: cs(el).fontWeight,
fontFamily: cs(el).fontFamily.split(',')[0].trim(),
color: cs(el).color,
backgroundColor: cs(el).backgroundColor,
lineHeight: cs(el).lineHeight,
letterSpacing: cs(el).letterSpacing,
height: cs(el).height,
width: cs(el).width,
padding: cs(el).padding,
margin: cs(el).margin,
borderRadius: cs(el).borderRadius,
boxShadow: cs(el).boxShadow,
});
const nav = document.querySelector('nav');
return {
nav: props(nav),
};
}
Common Pitfalls
-
Storybook iframe: Always access through #storybook-preview-iframe. Queries on the top-level document won't find story content.
-
Multiple matching elements: Production pages often have duplicate elements (mobile vs desktop nav). Filter by display !== 'none' or check for the presence of child elements unique to the visible version.
-
React state in Storybook: You cannot trigger React state changes via dispatchEvent inside evaluate. Use Storybook story variants (e.g., --services-dropdown-open) or the play function in stories to set the desired state.
-
Color format differences: rgb(0, 132, 139) and #00848B are the same color. Compare numerically if needed, but getComputedStyle always returns rgb() or rgba() format on both sides.
-
Subpixel rendering: Values like 37.6406px vs 38px are acceptable matches. Don't chase fractional pixel differences.
-
Font family strings: getComputedStyle returns the full stack (e.g., Inter, sans-serif). Split on comma and compare only the first family.
Cleanup
No files are created on disk. All data lives in the evaluate return values and the conversation context.