From asynkron-devtools
Scans web pages for UI consistency issues including misalignment, bad contrast, near-miss colors, inconsistent spacing, typography mismatches, layout problems, and accessibility violations. Requires Chrome DevTools MCP.
npx claudepluginhub asynkron/asynkron-skills --plugin asynkron-devtoolsThis skill uses the workspace's default tool permissions.
This skill requires:
Reviews web app or page visual design for layout, typography, spacing, color, hierarchy, consistency, interactions, and responsiveness. Outputs polished findings report with screenshots.
Conducts 7-phase frontend design review with WCAG 2.1 AA accessibility checks, responsive testing, visual polish for PR reviews, UI audits, layout issues.
Audits usability of existing front-end code or live websites using 15 principles, identifies component/system issues, rates severity, and suggests fixes.
Share bugs, ideas, or general feedback.
This skill requires:
Check that the MCP tools are available:
mcp__chrome-devtools__list_pages (or mcp__claude-in-chrome__tabs_context_mcp)
If not available, the user needs to add the chrome-devtools MCP server to their Claude Code config.
UXly is a comprehensive UI consistency scanner that detects ~40 categories of design issues:
The scanner script is at skills/uxly/uxly-scanner.js in this repository.
The scanner script needs to be served via HTTP so it can be injected into pages. Start a CORS-enabled server:
# Find the skill directory containing uxly-scanner.js
UXLY_DIR="$(find /Users/rogerjohansson/git/asynkron/Skills/skills/uxly -name 'uxly-scanner.js' -exec dirname {} \; | head -1)"
# Kill any existing server on port 8765
lsof -ti:8765 | xargs kill -9 2>/dev/null; sleep 1
# Start CORS-enabled HTTP server
python3 -c "
import http.server, os
class CORSHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
super().end_headers()
os.chdir('$UXLY_DIR')
http.server.test(HandlerClass=CORSHandler, port=8765, bind='127.0.0.1')
" &
Verify the server is running:
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8765/uxly-scanner.js
Expected: 200
For each page to scan:
Use mcp__chrome-devtools__navigate_page or mcp__chrome-devtools__select_page to ensure the target page is selected.
// Use mcp__chrome-devtools__evaluate_script with this function:
async () => {
document.querySelectorAll('script[src*="8765"]').forEach(s => s.remove());
delete window.uxlyResult;
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'http://127.0.0.1:8765/uxly-scanner.js?' + Date.now();
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
const r = window.uxlyResult;
if (!r) return { error: 'No result' };
return {
url: r.url,
score: r.score,
total: r.findings.length,
counts: r.summary.findingCounts,
findings: r.findings.map(f => ({
sev: f.severity,
cat: f.category,
msg: f.message
}))
};
}
Focus on errors first, then warnings. Info items are suggestions.
For each actionable finding:
For pages with semi-transparent layers, gradients, or stacked backgrounds, use the pixel-sampling workflow for accurate contrast measurement:
// mcp__chrome-devtools__evaluate_script:
() => {
const count = window.uxlyHideText();
return { hidden: count };
}
Use mcp__chrome-devtools__take_screenshot to save a JPEG to the server directory:
filePath: <UXLY_DIR>/uxly_bg_screenshot.jpg
format: jpeg
quality: 50
// mcp__chrome-devtools__evaluate_script:
async () => {
window.uxlyRestoreText();
const resp = await fetch('http://127.0.0.1:8765/uxly_bg_screenshot.jpg?' + Date.now());
const blob = await resp.blob();
const dataUrl = await new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
const refined = await window.uxlyRefineWithScreenshot(dataUrl);
return {
score: refined.score,
contrastMethod: refined.contrastMethod,
contrastIssues: refined.analyses.contrast.length,
findings: refined.findings.map(f => ({ sev: f.severity, cat: f.category, msg: f.message }))
};
}
After injecting the scanner on any page, extract navigation links:
// mcp__chrome-devtools__evaluate_script:
() => {
const links = document.querySelectorAll('a[href]');
return [...links]
.filter(l => l.href.includes(location.host))
.map(l => ({ href: l.getAttribute('href'), text: l.textContent.trim() }))
.filter((v, i, a) => a.findIndex(x => x.href === v.href) === i);
}
Navigate to each URL and run Workflow A. Collect scores.
Present a table of page scores and highlight cross-page patterns:
| Issue Category | Typical Fix |
|---|---|
near-miss-color | Unify to single design token (check tokens.css dark theme) |
low-contrast | Darken bg with color-mix(in srgb, var(--color) X%, black) or use white text |
too-many-sizes | Set explicit font-size: var(--app-type-*) on component |
inconsistent-padding | Standardize to spacing scale (8, 12, 16, 20, 24px) |
misaligned-siblings | Check align-items on flex parent, remove stray margin-top |
inconsistent-icon-size | Set explicit width/height on SVG icons in same context |
misaligned-icon | Use align-items: center on flex parent |
nested-panel | Remove inner border/shadow or flatten to single container |
blocked-interactive | Fix z-index stacking or remove overlay |
missing-label | Add aria-label or <label> element |
tiny-tap-target | Increase to minimum 24x24px, or wrap in larger clickable area |
cramped-padding | Increase padding to at least 50% of font-size |
::deep: Blazor scoped CSS (.razor.css) only applies to elements rendered directly in that component. Labels/inputs inside child components (e.g., MudBlazor MudSelect) need ::deep to pierce the boundary.overrides.css with !important and design tokens.var(--app-type-*) over hard-coded pixel sizes. The full type scale: --app-type-2xs (10px), --app-type-xs (11px), --app-type-sm (12px), --app-type-md (13px), --app-type-base (14px), --app-type-lg (15px), --app-type-xl (16px), --app-type-2xl (18px).