ui-check
How to compare a Brain Games page with the approved design in docs/design.html using Chrome DevTools MCP. Use after any change to a .vue file, a CSS file in app/assets/css, or game JSON copy, when the
Files1
View on GitHubSKILL.md.claude/skills/ui-check/SKILL.md
GitHubUI check against docs/design.html
docs/design.html is the approved design. The Nuxt app must match it. Compare the two side by side, state by state.
1. Start the servers
- App: check if
http://localhost:3000answers (curl -s -o /dev/null -w '%{http_code}' http://localhost:3000). If not, start it in the background:
Wait until it answers. Note the port it prints if 3000 is taken. Stop it when you are done only if you started it.export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"; . "$NVM_DIR/nvm.sh"; nvm use --silent; npm run dev - Design: open
file://<repo>/docs/design.htmldirectly. If Chrome blocks it, serve it:python3 -m http.server 4321 -d docsand usehttp://localhost:4321/design.html.
2. Map routes to design views
design.html routes by hash.
| App route | Design view |
|---|---|
/ |
design.html# |
/games |
design.html#games |
/games/<id> |
design.html#<id> |
/profile |
design.html#profile |
/anything-else |
no design view; check the friendly 404 copy and "Back to games" button |
3. Use your own Chrome tab
All agents share one Chrome.
new_pagefor the app andnew_pagefor the design. Remember both page ids.- Call
select_pagebefore every action. Another agent may have switched tabs. close_pageon both tabs when you finish, even after an error.
4. Set the same state on both sides
Both read and write localStorage key bgarden, so the same state works on both.
- Seed state with
evaluate_script:
then reload. Use the same seed on both tabs. Test once with an empty store as well.localStorage.setItem( 'bgarden', JSON.stringify({ name: 'Ana', av: '🦉', best: { math: 12 }, hist: [], days: [], xp: 130, theme: 'light' }) ) - Theme: set
themein the seed tolight,darkorauto, then reload. For a quick switch without reload, setdocument.documentElement.setAttribute('data-theme', 'dark'), or remove the attribute for auto. - Auto theme: use
emulatewithprefers-color-scheme: darkand thenlight, withtheme: 'auto'.
5. The matrix
For each page, take a screenshot of app and design at:
| Viewport | Themes |
|---|---|
| 360×800 (mobile) | light, dark, auto (with system dark) |
| 1280×800 (desktop) | light, dark, auto (with system dark) |
Use resize_page or emulate for the viewport. Full-page screenshots for scrolling pages; viewport screenshots for game pages.
For a game page, check three states: ready (with the demo running), playing (after a few moves), and result. Play the same moves on both sides where you can. For a result, a quick way is to finish fast or let the clock run.
6. What to compare
Look at the screenshots side by side and list concrete differences:
- Layout: grid columns, tile sizes (
big,wide,tall,normal), sidebar position, stacking at ≤1000px and ≤640px. - Copy: headings, labels, buttons, band labels, messages. Copy should come from the game JSON.
- Colour: tokens and category tints. Compare computed values with
get_css_styleswhen unsure. - Type: Baloo 2 for display, numbers and buttons; Lexend for body. Sizes and weights.
- Shape: radius 22px cards, 18px buttons, 14px icons; the pressable shadow on primary buttons.
- Motion: demo frames loop every 2.2s; confetti on a new best; none with reduced motion.
Ignore differences that come from different random values (questions, shuffles, daily picks).
7. Rules checklist (CLAUDE.md rules 7 to 9)
Check each item with evaluate_script or get_css_styles, not by eye alone.
- Tokens only. Tailwind utilities that map to the tokens, or CSS variables. Look for hard-coded colours in the changed components:
grep -nE '#[0-9a-fA-F]{3,8}\b|rgb\(|hsl\(' app/components app/pages. Onlytokens.cssdefines colours. - Both themes work. No unreadable text, no white boxes in dark mode, no missing tints. Check contrast of body text and buttons.
- Touch targets ≥44px at 360px wide:
;[...document.querySelectorAll('button, a, input, [role=button]')] .map(e => [e, e.getBoundingClientRect()]) .filter(([, r]) => r.width && (r.width < 44 || r.height < 44)) .map(([e, r]) => `${e.outerHTML.slice(0, 80)} ${r.width}x${r.height}`) - Bottom tab bar does not cover content at 360px: scroll to the bottom; the last element's bottom edge must sit above the tab bar's top edge.
- Inputs have font-size ≥16px (stops iOS zoom):
;[...document.querySelectorAll('input, textarea, select')].map(e => getComputedStyle(e).fontSize) - Visible
:focus-visible. Press Tab through the page withpress_key. Every focused control shows a ring. Take a screenshot of at least one. - Keyboard play. On a game page: Enter starts the game, the game is playable with Enter/Space/typing only, and Enter on the result replays.
- Real controls. Every clickable thing is a
<button>or<a>. Icon-only buttons havearia-label. The active nav tab hasaria-current="page". - No horizontal scroll at 360px:
document.documentElement.scrollWidth <= innerWidth. - Console clean.
list_console_messagesshows no errors or Vue warnings.
8. Report
One block per finding:
[HIGH|MEDIUM|LOW] <route> @ <viewport> <theme>: <what is different or broken>
App: <what the app shows> Design: <what design.html shows>
Rule: <CLAUDE.md rule number, if any> Where: <file:line if you can find it>
Evidence: <screenshot path or measured value>
- HIGH: broken behaviour, a rule 7 to 9 violation, unreadable in a theme, content hidden.
- MEDIUM: clear visual difference from the design (layout, colour, copy, size).
- LOW: small spacing or polish differences.
End with a verdict: PASS (no HIGH or MEDIUM) or FAIL, and the list of pages and states you checked.