Visual Design Audit Skill
Build passing ≠ design good. The render must be seen and judged before shipping.
This skill walks the screen against two axes — 심미성(aesthetic quality) and 유저 편의성(usability) — using 16 lenses from canonical design literature.
1. When to fire
Always fire at one of these moments:
- Final step of any UI work, before "complete" / PR open
- After a token / theme / typography change that touches multiple components
- Before declaring a design system migration "done"
- When the user asks "디자인 어때 / 괜찮아 보여 / 디자인 검토해줘"
- When you have no reference image (if you have one, run
visual-reference-compare in addition — they're complementary, not duplicate)
Skip when:
- The change is non-visual (data layer, build config, docs only)
- A more specialized skill has already covered this surface (e.g.,
visual-reference-compare was just run on the same screen with a reference)
- The user explicitly says "skip the visual check"
2. Capture
Use the available tooling in this order:
- Inside cmux →
cmux-browser skill (cmux browser surface:N screenshot /tmp/x.png)
- Without cmux →
agent-browser, Playwright, or the screenshot skill
- Always capture multiple breakpoints:
- Desktop ≥ 1280px (the layout the design probably started from)
- Tablet ~ 768px (one or two columns collapse here)
- Mobile ~ 393px (Pretendard / Hangul wrap behavior changes)
- Always capture key states when relevant:
- default / hover / focus-visible / active / disabled
- empty / loading / error
- first-load vs. populated (skeleton vs. content)
- Capture at 1× and 2× DPR if the design relies on hairlines / fine typography.
Don't audit from memory. The whole point is seeing.
2-A. Find the real scroll container before screenshotting
A surprisingly common time-sink: you call window.scrollTo(0, 0) or cmux browser scroll --y 0, the viewport doesn't move, you screenshot, and the screenshot shows the middle of the screen — not the top. Cause: the page isn't scrolling window. It's scrolling an inner container (e.g., a phone-frame <section aria-label="홈"> > <div> with overflow: auto).
Detection order before the first screenshot:
// 1. Is window scroll actually moving?
({ winY: window.scrollY, docH: document.documentElement.scrollHeight, vh: innerHeight })
// 2. If winY stays 0 / docH ≈ vh but content is clearly longer, look for inner scroll
Array.from(document.querySelectorAll('*'))
.filter(el => el.scrollHeight > el.clientHeight + 4 && getComputedStyle(el).overflowY !== 'visible')
.map(el => ({ tag: el.tagName, label: el.getAttribute('aria-label'), sh: el.scrollHeight, ch: el.clientHeight }))
Then scroll the real container by reference, not window:
const real = document.querySelector('section[aria-label="홈"] > div'); // or whatever your selector
real.scrollTo({ top: 0, behavior: 'instant' });
Mobile-app-style designs hosted in a fixed-phone-frame container almost always need this. Skip this step and every screenshot will be the wrong region of the page.
2-B. Verify scroll position succeeded
After every scroll, re-read scrollTop before screenshotting. cmux browser scroll --y 0 can silently no-op if the wrong element is the scroll root. One line of paranoia saves a full pass of audit.
3. The two axes
| Axis |
Question |
Outcome |
| 심미성 (Aesthetics) |
"이 화면을 처음 본 사람이 시각적으로 정돈됐다고 느끼는가?" |
hierarchy / rhythm / balance / palette |
| 유저 편의성 (Usability) |
"처음 본 사람이 다음에 뭘 해야 할지 즉시 알고, 그렇게 할 수 있는가?" |
affordance / contrast / target size / status |
Both must pass. Beautiful but unusable, or usable but ugly, is a fail.
4. 심미성 lenses (6)
4.1 Visual hierarchy — Refactoring UI ch. 2
"Hierarchy isn't about making things bigger; it's about making important things stand out."
Check:
- Is there a clear first thing the eye lands on? (hero / primary headline)
- Are H1 → H2 → body at least 1.5× step in size, or compensated by weight / color?
- Is body text bolder/darker than secondary/meta text?
- Are size + weight + color used together (not size alone)?
Bad signals:
- All headings the same size; only color separates them
- Body text gray-on-white (
#aaa on #fff) — fails WCAG AA + flattens hierarchy
- 16px body, 18px H3 — almost no perceived difference
- Multiple "primary" buttons competing
Good ref: Stripe docs, Linear changelog, Vercel marketing — body at 17-19px, headlines at 32-56px with strong weight contrast.
4.2 Spacing rhythm — Refactoring UI ch. 3, Every Layout, 8-Point Grid (Bryn Jackson)
"Start with too much whitespace and remove until it feels right — not the other way."
Check:
- Are spacings drawn from a scale (4 / 8 / 12 / 16 / 24 / 32 / 40…) or arbitrary px?
- Does the scale follow an 8pt grid (multiples of 8) with optional 4pt baseline grid for typography? — industry standard for scalability across DPRs.
- Is vertical rhythm consistent across sections (e.g., every section uses the same large spacer)?
- Is
gap used on parent containers, not margin on every child?
- Does the screen have breathing room at the page edges (≥ 24px on mobile, ≥ 40px on desktop)?
- Is
line-height a multiple of the base unit (e.g., 24px line-height for 16px text on an 8pt grid)?
- Micro vs macro spacing ratio — micro (gaps inside a card / between paragraph lines) should be ≈ 1/3 of macro (gaps between sections). If a card's internal padding equals the gap between cards, the cards visually merge.
Bad signals:
- One section has 56px below, the next has 32px, the next has 48px — random
- Cards crammed against each other with no separation
- Page content touches the viewport edge
- Internal card padding == external card gap → cards visually bleed into one mass
- 1px / 3px / 7px-style spacings → not on any scale
Good ref: Apple newsroom, Notion docs — generous, rhythmic spacing.
4.3 Color & contrast — Refactoring UI ch. 4, WCAG 2.2
"Use a small set of semantic colors. Don't decorate with color; communicate with it."
Check:
- Palette size: ≤ 3 brand colors + neutrals + 2-3 status (success/warning/danger)?
- Body text vs. background contrast ≥ 4.5:1 (WCAG AA), large text ≥ 3:1?
- Interactive elements visually distinct from static ones?
- Does color encode meaning (status, action) consistently?
Quick in-page contrast probe (run in console / cmux eval — no DevTools needed):
function relLum(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const t = (c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
return 0.2126 * t(r) + 0.7152 * t(g) + 0.0722 * t(b);
}
function ratio(fgHex, bgHex) {
const a = relLum(fgHex), b = relLum(bgHex);
return ((Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)).toFixed(2);
}
// Example: ratio('#5e6470', '#f6f4ef') → "5.83" (passes AA body)
Flag any text where measured ratio < required threshold and route to css-judgment for a token-level fix (not a one-off color override at the call site).
Bad signals:
- 14px gray-on-gray text "for elegance" — fails AA
- Brand color used for everything (buttons, links, dividers, icons) — semantics dilute
- Hover state same color as default — no feedback
Tools: macOS Digital Color Meter / Chrome DevTools "Contrast" / npx pa11y <url>.
4.4 Alignment, balance & visual tension — Don Norman, Refactoring UI, Smashing (compositional balance)
"Optical alignment trumps mathematical alignment. Symmetric balance is calm; asymmetric balance is alive."
Tension & focal point (composition layer):
- Is there one clear focal point the eye lands on first, and is it deliberately placed (not center-by-default)?
- If the composition is asymmetric, are the visual weights balanced (one heavy element vs. several lighter ones / dense vs. spacious / dark vs. light)?
- Are there leading lines (image edges, type baselines, dividers) that guide the eye to the focal point?
- Is there at least one dominant axis the layout commits to (vertical column for editorial, diagonal for energetic, horizontal for narrative)? "Floats randomly" reads as undesigned.
- Are complementary contrasts present where tension is wanted (light vs. dark, dense vs. sparse, large vs. small, color vs. mono)? Same-weight everywhere = flat.
Alignment (measurement layer):
Check:
- Are siblings (cards, list items) left-edge aligned? (measure with
getBoundingClientRect().left — do not eyeball)
- Are cards in a grid same height (or intentionally different — never accidentally)?
- Are cards in a grid same width? (a single card narrower than its peers usually means it didn't fill its grid cell — child needs
width: 100% or grid template fix)
- Is whitespace distributed (no card stretched into empty band)?
- Does the overall composition have a clear focal point (eye anchor)?
- Section vertical padding symmetry — the gap above and below the page content should match unless deliberately asymmetric. Measure
frame.getBoundingClientRect() vs. firstSection.getBoundingClientRect().top and lastSection.getBoundingClientRect().bottom. A page with padding-top: 32px / padding-bottom: 16px reads as "the designer forgot."
Bad signals:
- One card stretches to match a sibling and gains an empty trailing band →
align-items: stretch smell, see css-judgment Example 8
- Two-column grid with one column overflowing →
min-width: 0 smell, see css-judgment Example 5
- Card narrower than its peers in the same row → flex/grid child not filling its track (the
<button> shrinking to label width is the most common variant — <button> defaults to inline-block sizing, so even inside flex-direction: column it shrinks to its content; force with width: 100% or align-items: stretch on the parent)
- Centered headings but left-aligned body inside the same container
- Page top padding ≠ page bottom padding (asymmetric without intent)
4.5 Typography quality — A List Apart, Refactoring UI
"Get the body right first. Everything else is fine-tuning."
Check (English/Latin):
- Body 16-19px / line-height 1.5-1.7 / max-width 65-75 char
- Headings tighter line-height (1.05-1.2) / slightly negative letter-spacing
- One or two type families max (sans for UI; serif/mono for editorial accents)
Check (Korean):
- Run
korean-typography skill for the full checklist (Pretendard, line-height 1.7+, word-break: keep-all, 받침 충돌 가드)
Bad signals:
- Body < 15px (Latin) / < 16px (Korean) — looks small even when "design article said so"
- All text in the same weight (visual monotony)
- Justified text with awkward gaps
- Long URL / 어절 forcing horizontal scroll → missing
overflow-wrap: break-word
4.6 Image use & composition — A List Apart, Refactoring UI, editorial photography best practices
"An image is a co-author of the headline. If it doesn't argue for the page, it's filler."
Check:
- Aspect ratio: hero ≥ 1.7:1 (16:9 baseline) on desktop, taller crop (4:5 or 3:4) on mobile. Aspect should be a design decision, not a CMS upload accident.
- Focal point: subject in the rule-of-thirds anchor, not always center. If text overlays the image, the focal point and the text occupy different thirds.
- Text-over-image contrast: WCAG 4.5:1 against the actual pixels behind the text (not the average). Use a dim overlay or vignette if the image is busy.
- Photographic vs. illustration choice: editorial moodshot for emotion, illustration for abstract concepts, photography for product. Don't mix all three on one page without a clear hierarchy of voice.
- Mobile crop survives: the desktop hero usually crops to mobile with the right 30% lost. Verify the focal point still reads at the mobile crop.
- Image-to-text ratio per fold: an entirely-text fold reads as a wall; an entirely-image fold reads as advertising. Aim for roughly 40-60% image weight in editorial sections, 0% in dense info sections — but pick deliberately, not by accident.
- Loading strategy: hero
loading="eager" + fetchpriority="high"; below-fold loading="lazy". Mis-set lazy on hero = visible pop-in.
Bad signals:
- Hero is a generic stock photo of laptops/handshakes — no editorial voice
- Text sits awkwardly over a busy region of the image (no overlay, no contrast)
- Every image is the same 16:9 aspect and same focal placement — visual monotony
- Hero focal point gets cropped out on mobile
4.7 Proportion & negative space — Vanseo Design (golden section), A List Apart (modular typography), IxDF (white space)
"Whitespace is not absence; it is composition. Proportion is not aesthetics; it is structure."
Proportion & modular scale:
- Type sizes drawn from a modular scale ratio (1.250 Major Third / 1.333 Perfect Fourth / 1.5 Augmented Fourth / 1.618 Golden Ratio)? Each step is a deliberate multiple, not a guess.
- Body / lede / heading sizes preserve the ratio across breakpoints?
- Hero column-to-side-column width ratio reads as intentional (e.g., 1.618 : 1 for golden, 1.2 : 1 for near-equal, 2 : 1 for emphasis)?
Negative space (Micro vs Macro):
- Macro whitespace (between major sections, page margins, around hero) — generous, defines the page's calm
- Micro whitespace (between lines, around paragraph, inside cards) — tight enough that grouping is preserved
- Ratio: micro ≈ 1/3 of macro. If they're equal, every group blurs into the next.
- Gestalt proximity check: items in the same group should be visually closer to each other than to items in a different group.
- Page edge margins scale with viewport (e.g., 16-24px mobile / 40-80px desktop / centered max-width ≤ 1200px on ultra-wide).
Bad signals:
- Type sizes are 15 / 16 / 18 / 22 / 24 / 28 / 36 — no consistent ratio, looks like trial-and-error
- Section spacing == card spacing == paragraph spacing — everything blurs together (Gestalt proximity broken)
- Page margins are 0 — content hugs viewport edge, no breathing room
- Excessive whitespace inside cards but cards crammed against each other (proportion inverted)
- Hero columns split 50/50 by default — perfectly symmetric is often the least interesting choice
4.8 Distinctiveness — frontend-design skill alignment
"Avoid the generic AI aesthetic: centered hero / 3-card grid / purple gradient / 'Get Started' CTA."
Check:
- Does the screen have a point of view (a specific style choice you can name)?
- Or does it look like every other AI-generated landing page?
- Is at least one element doing something unexpected but intentional (asymmetry, oversized number, editorial pull-quote, photographic moodshot)?
Bad signals:
- Three identical cards in a row, each with icon + title + paragraph + "Learn more"
- Hero is centered headline + subhead + gradient button + gradient blob in background
- Purple-to-pink gradient with no other color discipline
If the screen feels generic, that's not a polish issue — it's a design issue. Surface it.
5. 유저 편의성 lenses (6)
Based on Nielsen Norman Group's 10 Usability Heuristics (Jakob Nielsen, 1994 → still definitive), distilled to the six that show up in screenshots:
5.1 Affordance — Don Norman, "Design of Everyday Things"
"An object's appearance should suggest how it's used."
Check:
- Is every clickable element visually distinct from static text? (color / underline / button shape)
- Do buttons look pressable (filled or outlined, not just text)?
- Is the primary action the most prominent thing on the screen?
- Are icons accompanied by labels (unless they're universally recognized — search, close, menu)?
Bad signals:
- Inline link looks identical to body text
- "Button" is just colored text, no shape
- Two buttons with the same weight ("Cancel" vs "Submit" both gray outline)
5.2 Visibility of system status — NN/g #1
"The system should always keep users informed about what is going on, through appropriate feedback within reasonable time."
Check:
- Is loading state visible (skeleton / spinner / progress)?
- Is success/error feedback visible (toast / banner / inline message)?
- Does the URL / breadcrumb / page title reflect where you are?
- Is selected/active state obvious (nav highlight, tab active border)?
Bad signals:
- Click button → nothing happens for 2s, no feedback
- Form submits silently with no confirmation
- Active nav item indistinguishable from inactive
5.3 Touch target & hit area — Apple HIG / Material Design
"Touch targets should be at least 44×44 pt (iOS) / 48×48 dp (Android)."
Check:
- Every interactive element ≥ 44×44 pt (use DevTools to measure)?
- Adequate spacing between adjacent targets (≥ 8 pt) to prevent mis-taps?
- Are touch targets sized for the action's importance (primary CTA bigger than secondary)?
Bad signals:
- Icon-only buttons at 24×24 — too small for thumbs
- Two checkboxes 4px apart — adjacent taps overlap
5.4 Readability — A List Apart, WCAG 2.2
"If users can't comfortably read your text, nothing else matters."
Check (overlap with 심미성 4.5):
- Body ≥ 16-17px (한글이면 17px+)
- Contrast ≥ 4.5:1 (or 3:1 for large/heading text)
- Line length 45-75 char (
max-width: ~65ch rule of thumb)
- Line-height 1.5+ (한글 1.7+)
- Not all-caps for body (only short labels)
Bad signals:
- Cramped body filling the entire viewport width (95 char per line)
- Gray text on colored card background → low contrast
- Hangul body at 14px — under-sized for the script
5.5 Navigation clarity — NN/g #6, #4
"Show users what they're looking at, what they can do, and how they can get back."
Check:
- Is there always a way back? (header logo, breadcrumb, browser back works)
- Is the current section/tab visibly marked?
- Are nav items grouped logically (no surprise ordering)?
- Does the layout follow platform conventions (e.g., bottom nav on mobile, sidebar on desktop)?
Bad signals:
- Hash routing breaks browser back button
- Tab UI without a clear "current" state
- Floating CTA covers content with no way to dismiss
5.6 Carousel / horizontal scroll — Apple HIG (Scroll Views), Material (Horizontal scrollers)
"The first item in a horizontal scroller should align with the rest of the page's content edge — and the last item should have the same trailing breathing room as the leading one."
Check:
- First card's left edge = the page content's left edge? (use
getBoundingClientRect().left on the first card vs. the page frame's content edge)
scroll-padding-inline-start / padding-inline-start on the scroller? Without it, when a user swipes back, the first card snaps flush to the viewport edge with zero breathing room.
- Last card's right edge has the same breathing room as the first card's left? Or is it amputated by
overflow: hidden?
- Scroll snap behaves on slow swipe? Not jumping past the intended card?
Bad signals:
- First card touches the viewport edge (no leading padding) — most common; very visible on mobile
- Last card's right edge is exactly at viewport edge → user can't tell if there's more
overflow-x: auto without scroll-padding → snap brings cards flush to edge
- Different padding on left vs. right of the strip
Fix recipe (CSS):
.carousel {
display: flex;
gap: var(--space-md);
overflow-x: auto;
scroll-snap-type: x mandatory;
/* leading / trailing breathing room */
padding-inline: var(--space-lg);
/* preserve breathing room on snap-back */
scroll-padding-inline: var(--space-lg);
}
.carousel > * { scroll-snap-align: start; }
5.7 Micro-interaction & feedback — Apple HIG (Animation), NN/g #1
"A polished UI has motion. Not flashy motion — confirmation motion."
Check:
- Every interactive element has a visible state change on hover (desktop) and active/press (mobile)? (subtle color shift, scale 0.97 on press, border emphasis — pick a small, consistent vocabulary)
- Cards / buttons feel "alive" on hover, or are they static rectangles?
- Critical feedback motions are present: progress bar fill animation, save toast slide-in, optimistic UI update, skeleton shimmer
- Restraint: motion ≤ 200ms (NN/g),
prefers-reduced-motion respected, no decorative loops competing with content
Bad signals:
- Buttons are visually inert until clicked (no hover, no press) — feels unresponsive
- All motion happens in 800ms eases — feels sluggish
- Decorative parallax / scroll-jacking on a content site — distracting
:hover defined but no :focus-visible — keyboard users get nothing
Patterns to add (light, default these in):
.interactiveCard {
border: 1px solid var(--border-card);
transition:
transform 0.12s ease,
background-color 0.12s ease,
border-color 0.12s ease,
border-width 0.12s ease;
}
.interactiveCard:hover {
background-color: var(--surface-hover);
/* Hover border 강화: 두꺼워지고 theme color로 — 카드가 '집어지는' 느낌 */
border: 2px solid var(--accent-brand);
/* 두께가 1→2px로 늘면 1px만큼 contents가 밀려나므로 padding을 1px 줄여 보정 */
padding: calc(var(--space-md) - 1px);
}
.interactiveCard:active { transform: scale(0.98); }
.interactiveCard:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.interactiveCard { transition: none; }
.interactiveCard:active { transform: none; }
}
Hover의 visual weight 변화는 두 가지 같이 가야 자연스럽다:
- color shift(미세한 배경 톤 변화)
- border 강화(두께 +1px + theme accent color)
color shift만 있으면 평평하고, border만 두꺼워지면 보더가 갑자기 튀어나온다. 둘이 동시에 변할 때 카드가 "들려" 보인다.
5.8 Empty / error / edge states — NN/g #9
"Help users recognize, diagnose, and recover from errors."
Check:
- Empty state has a prompt ("아직 데이터가 없어요 → 첫 항목 만들기"), not a blank page
- Error state explains what went wrong + what to do (not "Error 500")
- Loading state doesn't block the entire UI if avoidable (skeleton over spinner)
- 404 / unauthorized has a path back
Bad signals:
- Empty list with no message
- "Something went wrong" with no detail or retry
- Full-screen spinner for every action
6. Audit procedure
For each captured screenshot:
First impression (10s) — Note your gut reaction in one sentence. "What do I see first? What feels off?" This single sentence often surfaces the real issue faster than checklists.
Walk the 16 lenses — Don't skip ones that feel "obviously fine." The skip is where regressions hide.
Measure — don't eyeball. This is the single biggest lift in audit quality. Open the page in cmux/playwright and pull numbers, then compare to thresholds:
// Page frame / canvas
const frame = document.querySelector('section[aria-label="..."]');
frame.getBoundingClientRect();
// Every card in a row — check left/width parity
const cards = document.querySelectorAll('[role="listitem"]');
Array.from(cards).map(c => {
const r = c.getBoundingClientRect();
return { top: r.top, left: r.left, width: r.width, height: r.height };
});
// Vertical padding symmetry on the page
const first = document.querySelector('section:first-of-type');
const last = document.querySelector('section:last-of-type');
({ top: first.getBoundingClientRect().top, bottomGap: innerHeight - last.getBoundingClientRect().bottom });
// Touch target sizes
Array.from(document.querySelectorAll('button, a, [role="button"]')).map(el => {
const r = el.getBoundingClientRect();
return { label: el.textContent?.slice(0,20), w: r.width, h: r.height };
}).filter(b => b.w < 44 || b.h < 44);
// Contrast (rough — use DevTools "Contrast" for the official measurement)
getComputedStyle(document.querySelector('p')).color;
Patterns to compare against:
- All cards in a row →
width should be equal (±1px). Unequal means a child didn't fill its grid cell.
- All cards in a row →
left should be equal (or in monotonic carousel order).
- Vertical padding symmetry → top gap vs. bottom gap within 4px.
- Touch targets < 44×44 → flag.
- Body text contrast < 4.5:1 → flag.
Note severity per finding:
- blocker — fails WCAG, breaks usability, unreadable text, broken layout (overflow / cropping)
- major — clear UX issue but doesn't break the screen (low contrast, missing feedback, ambiguous CTA)
- minor — style inconsistency, slightly off rhythm, could be more distinctive
- nit — pure preference, ignore unless many accumulate
For each finding include: severity / axis / lens / location (component or screenshot region) / measured numbers / recommended fix / reference (which article / heuristic).
"정렬이 안 맞아 보여" 는 약함. "Card 3의 width가 156px인데 나머지 4개는 168px — <button>이 flex column 안에서 콘텐츠 폭으로 줄어듦. width: 100% 추가." 가 강함.
6.1 Self-critique loop (강제)
스크린샷을 본 직후, 다음 한 줄을 반드시 적는다 — "approve 직전 마지막 솔직한 평가":
"내가 이 화면을 처음 보는 사람이라면, '이 부분은 디자인이 좀 약하다'고 말할 곳은 어디인가?"
찾았다면 그 위치를 major 이상으로 분류한다. 자가 critique 없이 "다 괜찮다"로 끝나는 audit은 lens를 walk했더라도 실패한 audit이다. 이 한 줄은 보통 디자인 자체의 결함(레이아웃 결정 / 카드 컨셉 / 정보 위계)을 노출하며, 그건 css-judgment로는 못 잡고 재설계로만 잡힌다.
재설계가 필요하다고 판단되면 해당 컴포넌트는 frontend-design 스킬로 라우팅하고 "redesign 후보" 로 보고에 명시.
7. Report format
# Visual Design Audit — <screen / route>
## First impression
<one sentence>
## Findings (sorted by severity)
### blocker — <axis> — <lens>
- Location: <component path or screenshot annotation>
- Issue: <what's wrong, measured if possible>
- Fix: <specific css/component change>
- Ref: <article / heuristic / `css-judgment` Example N>
### major — ...
### minor — ...
## Passed lenses
- <lens>: ✓ <why it's good — short>
- ...
## Outstanding
<questions, scope creep risks, things needing user input>
Keep findings specific and measurable. "여백이 너무 많다" 는 약함. "heroCard 하단에 240px 빈 band, align-items: stretch 결과 — align-self: start로 fix" 는 강함.
8. Anti-patterns (audit itself)
- ❌ "Looks good to me" — that's not an audit. Walk the lenses.
- ❌ Skipping mobile capture because "desktop looks fine"
- ❌ Approving without measuring contrast / size when the issue is on that lens
- ❌ Lumping all findings as "minor"
- ❌ Suggesting fixes outside the screen (refactor X, rebuild Y) — stay on what the screenshot shows
- ❌ Writing the report without the screenshots inline / referenced
- ❌ Screenshotting without verifying scroll position succeeded —
cmux browser scroll --y 0 silently no-ops if the page uses an inner scroll container. Always re-read scrollTop after a scroll command. See §2-A / §2-B.
- ❌ Eyeballing alignment / width / padding parity — these are number questions. Use
getBoundingClientRect(); report exact px deltas. "Looks off" is not a finding.
- ❌ Drawing missing icons by hand inside the audit — if a screenshot reveals empty/broken icon slots or fake-photo SVGs, route through
image-asset-strategy → /codex:rescue (Codex owns imagegen, can run with --write to land files directly). Do not "just sketch a placeholder."
- ❌ Stopping at the first finding — full-pass the lenses even if you find a blocker early. Subsequent findings often share a root cause and fix together.
9. Relation to sibling skills
| Skill |
Relation |
visual-reference-compare |
Comparison against a supplied reference image. Run this skill in addition when you have a reference, not instead. |
css-judgment |
Where most of the recommended fixes will land. The audit finds issues; css-judgment guides the fix. Example 5 / 8 / 9 are common targets. |
korean-typography |
When body is Hangul, defer the typography lens (4.5 / 5.4) to its checklist. |
image-asset-strategy |
If the audit finds a fake SVG face / product / hero, route it back through asset-strategy → Codex / imagegen. |
frontend-design |
Distinctiveness lens (4.6) — if audit flags "looks AI-generic," the next iteration goes through frontend-design's "avoid generic aesthetic" guide. |
| NN/g 10 Heuristics |
https://www.nngroup.com/articles/ten-usability-heuristics/ — the canonical reference for §5 lenses. |
| WCAG 2.2 |
https://www.w3.org/TR/WCAG22/ — color contrast and target size compliance. |
| Apple HIG / Material Design |
Platform conventions for touch target, navigation, gestures. |
| Refactoring UI |
https://www.refactoringui.com/ — the canonical reference for §4 lenses. |
10. When the audit blocks ship
If you find ≥ 1 blocker, do not approve. Either:
- Fix it now (route to
css-judgment / korean-typography / etc.)
- Or report it clearly and ask the user before shipping
If you find only major / minor / nit, list them in the punch list and let the user decide what to ship-now vs. file-for-later.
11. References (one-line each)
- Refactoring UI — Wathan & Schoger. Visual hierarchy, spacing, color, typography. Highest density of actionable advice.
- NN/g 10 Usability Heuristics — Nielsen. The default usability vocabulary.
- The Design of Everyday Things — Norman. Affordance, signifier, feedback.
- A List Apart — typography, readability essays (Tim Brown's "More Meaningful Typography" etc.).
- WCAG 2.2 — color contrast, text size, target size compliance.
- Apple Human Interface Guidelines — touch target, platform conventions.
- Material Design 3 Guidelines — touch target, motion, accessibility.
- Every Layout — Bell & Andrew. CSS layout primitives and rhythm.
- Tufte, Edward — Visual Display of Quantitative Information. Density, signal/noise.
- 8-Point Grid (Bryn Jackson, Spec.fm) — multiples-of-8 spacing scale + 4pt baseline grid for typography.
- Modular scale (Tim Brown / A List Apart) — type sizes drawn from a ratio (1.250 / 1.333 / 1.5 / 1.618).
- Golden ratio in UI (Figma resource library, NN/g) — 1.618 as a column / typography / button proportion.
- Negative space in design (IxDF) — micro vs macro whitespace; micro ≈ 1/3 of macro; Gestalt proximity for grouping.
- Compositional balance (Smashing Magazine) — symmetric vs. asymmetric balance; visual tension via weight contrast and leading lines.
- Hero image best practices (Shopify / Squarespace blog) — 16:9 baseline, mobile crop survival, focal at rule-of-thirds, text/image contrast.
- 한국 디자인 블로그 — toss / 카카오디자인 / 우아한형제들 / karrot — Hangul-aware patterns to compare against.
1---2name: visual-design-audit3description: Use at the end of any UI work — right before reporting "done" or opening a PR — to audit a rendered screen against two axes (심미성 / 유저 편의성). Captures viewport screenshots via cmux / agent-browser / playwright, then walks 16 lenses derived from Refactoring UI, NN/g 10 Heuristics, WCAG, Apple HIG, A List Apart, and Don Norman's "Design of Everyday Things." Enforces measurement (`getBoundingClientRect`, contrast, target-size in numbers) over eyeball judgment, with explicit guards for inner-scroll-container traps, carousel/swipe padding parity, page-vertical symmetry, and flex content-size shrinking. Returns a prioritized punch list with severity + lens + measured deltas + recommended fix. Distinct from `visual-reference-compare` — that skill compares a render against a supplied reference image; this skill judges whether the render is good design on its own terms. Triggers when no reference is supplied, when the user says "디자인 어때 / 괜찮아 보여 / 검토해줘 / 패딩 안 맞는 거 확인해", and as the standard final gate after frontend work.4---56# Visual Design Audit Skill78> Build passing ≠ design good. The render must be **seen and judged** before shipping.9> This skill walks the screen against two axes — **심미성**(aesthetic quality) and **유저 편의성**(usability) — using 16 lenses from canonical design literature.1011---1213## 1. When to fire1415**Always fire** at one of these moments:1617- Final step of any UI work, before "complete" / PR open18- After a token / theme / typography change that touches multiple components19- Before declaring a design system migration "done"20- When the user asks "디자인 어때 / 괜찮아 보여 / 디자인 검토해줘"21- When you have no reference image (if you have one, run `visual-reference-compare` in addition — they're complementary, not duplicate)2223**Skip** when:24- The change is non-visual (data layer, build config, docs only)25- A more specialized skill has already covered this surface (e.g., `visual-reference-compare` was just run on the same screen with a reference)26- The user explicitly says "skip the visual check"2728---2930## 2. Capture3132Use the available tooling in this order:33341. **Inside cmux** → `cmux-browser` skill (`cmux browser surface:N screenshot /tmp/x.png`)352. **Without cmux** → `agent-browser`, Playwright, or the `screenshot` skill363. **Always capture multiple breakpoints**:37 - **Desktop** ≥ 1280px (the layout the design probably started from)38 - **Tablet** ~ 768px (one or two columns collapse here)39 - **Mobile** ~ 393px (Pretendard / Hangul wrap behavior changes)404. **Always capture key states** when relevant:41 - default / hover / focus-visible / active / disabled42 - empty / loading / error43 - first-load vs. populated (skeleton vs. content)445. **Capture at 1× and 2× DPR** if the design relies on hairlines / fine typography.4546Don't audit from memory. The whole point is *seeing*.4748### 2-A. Find the real scroll container before screenshotting4950A surprisingly common time-sink: you call `window.scrollTo(0, 0)` or `cmux browser scroll --y 0`, the viewport doesn't move, you screenshot, and the screenshot shows the middle of the screen — not the top. Cause: the page isn't scrolling `window`. It's scrolling an **inner container** (e.g., a phone-frame `<section aria-label="홈"> > <div>` with `overflow: auto`).5152**Detection order** before the first screenshot:5354```js55// 1. Is window scroll actually moving?56({ winY: window.scrollY, docH: document.documentElement.scrollHeight, vh: innerHeight })5758// 2. If winY stays 0 / docH ≈ vh but content is clearly longer, look for inner scroll59Array.from(document.querySelectorAll('*'))60 .filter(el => el.scrollHeight > el.clientHeight + 4 && getComputedStyle(el).overflowY !== 'visible')61 .map(el => ({ tag: el.tagName, label: el.getAttribute('aria-label'), sh: el.scrollHeight, ch: el.clientHeight }))62```6364Then scroll the **real container** by reference, not `window`:6566```js67const real = document.querySelector('section[aria-label="홈"] > div'); // or whatever your selector68real.scrollTo({ top: 0, behavior: 'instant' });69```7071Mobile-app-style designs hosted in a fixed-phone-frame container almost always need this. Skip this step and every screenshot will be the wrong region of the page.7273### 2-B. Verify scroll position succeeded7475After every scroll, **re-read `scrollTop`** before screenshotting. `cmux browser scroll --y 0` can silently no-op if the wrong element is the scroll root. One line of paranoia saves a full pass of audit.7677---7879## 3. The two axes8081| Axis | Question | Outcome |82|---|---|---|83| 심미성 (Aesthetics) | "이 화면을 처음 본 사람이 시각적으로 정돈됐다고 느끼는가?" | hierarchy / rhythm / balance / palette |84| 유저 편의성 (Usability) | "처음 본 사람이 다음에 뭘 해야 할지 즉시 알고, 그렇게 할 수 있는가?" | affordance / contrast / target size / status |8586**Both must pass.** Beautiful but unusable, or usable but ugly, is a fail.8788---8990## 4. 심미성 lenses (6)9192### 4.1 Visual hierarchy — *Refactoring UI* ch. 29394> "Hierarchy isn't about making things bigger; it's about making important things stand out."9596Check:97- Is there a clear **first thing the eye lands on**? (hero / primary headline)98- Are H1 → H2 → body **at least 1.5× step** in size, or compensated by weight / color?99- Is body text bolder/darker than secondary/meta text?100- **Are size + weight + color used together** (not size alone)?101102Bad signals:103- All headings the same size; only color separates them104- Body text gray-on-white (`#aaa` on `#fff`) — fails WCAG AA + flattens hierarchy105- 16px body, 18px H3 — almost no perceived difference106- Multiple "primary" buttons competing107108Good ref: Stripe docs, Linear changelog, Vercel marketing — body at 17-19px, headlines at 32-56px with strong weight contrast.109110### 4.2 Spacing rhythm — *Refactoring UI* ch. 3, *Every Layout*, *8-Point Grid (Bryn Jackson)*111112> "Start with too much whitespace and remove until it feels right — not the other way."113114Check:115- Are spacings drawn from a **scale** (4 / 8 / 12 / 16 / 24 / 32 / 40…) or arbitrary px?116- Does the scale follow an **8pt grid** (multiples of 8) with optional **4pt baseline grid** for typography? — industry standard for scalability across DPRs.117- Is **vertical rhythm consistent across sections** (e.g., every section uses the same large spacer)?118- Is `gap` used on parent containers, not `margin` on every child?119- Does the screen have **breathing room** at the page edges (≥ 24px on mobile, ≥ 40px on desktop)?120- Is `line-height` a **multiple of the base unit** (e.g., 24px line-height for 16px text on an 8pt grid)?121- **Micro vs macro spacing ratio** — micro (gaps inside a card / between paragraph lines) should be ≈ 1/3 of macro (gaps between sections). If a card's internal padding equals the gap between cards, the cards visually merge.122123Bad signals:124- One section has 56px below, the next has 32px, the next has 48px — random125- Cards crammed against each other with no separation126- Page content touches the viewport edge127- Internal card padding == external card gap → cards visually bleed into one mass128- 1px / 3px / 7px-style spacings → not on any scale129130Good ref: Apple newsroom, Notion docs — generous, rhythmic spacing.131132### 4.3 Color & contrast — *Refactoring UI* ch. 4, *WCAG 2.2*133134> "Use a small set of semantic colors. Don't decorate with color; communicate with it."135136Check:137- Palette size: ≤ 3 brand colors + neutrals + 2-3 status (success/warning/danger)?138- Body text vs. background contrast ≥ **4.5:1** (WCAG AA), large text ≥ **3:1**?139- Interactive elements visually distinct from static ones?140- Does color encode meaning (status, action) consistently?141142Quick in-page contrast probe (run in console / cmux eval — no DevTools needed):143144```js145function relLum(hex) {146 const r = parseInt(hex.slice(1, 3), 16) / 255;147 const g = parseInt(hex.slice(3, 5), 16) / 255;148 const b = parseInt(hex.slice(5, 7), 16) / 255;149 const t = (c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);150 return 0.2126 * t(r) + 0.7152 * t(g) + 0.0722 * t(b);151}152function ratio(fgHex, bgHex) {153 const a = relLum(fgHex), b = relLum(bgHex);154 return ((Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)).toFixed(2);155}156// Example: ratio('#5e6470', '#f6f4ef') → "5.83" (passes AA body)157```158159Flag any text where measured ratio < required threshold and route to `css-judgment` for a token-level fix (not a one-off color override at the call site).160161Bad signals:162- 14px gray-on-gray text "for elegance" — fails AA163- Brand color used for everything (buttons, links, dividers, icons) — semantics dilute164- Hover state same color as default — no feedback165166Tools: macOS `Digital Color Meter` / Chrome DevTools "Contrast" / `npx pa11y <url>`.167168### 4.4 Alignment, balance & visual tension — *Don Norman*, *Refactoring UI*, *Smashing (compositional balance)*169170> "Optical alignment trumps mathematical alignment. Symmetric balance is calm; asymmetric balance is alive."171172**Tension & focal point** (composition layer):173- Is there **one clear focal point** the eye lands on first, and is it deliberately placed (not center-by-default)?174- If the composition is asymmetric, are the visual weights balanced (one heavy element vs. several lighter ones / dense vs. spacious / dark vs. light)?175- Are there **leading lines** (image edges, type baselines, dividers) that guide the eye to the focal point?176- Is there at least one **dominant axis** the layout commits to (vertical column for editorial, diagonal for energetic, horizontal for narrative)? "Floats randomly" reads as undesigned.177- Are complementary contrasts present where tension is wanted (light vs. dark, dense vs. sparse, large vs. small, color vs. mono)? Same-weight everywhere = flat.178179**Alignment (measurement layer):**180181Check:182- Are siblings (cards, list items) **left-edge aligned**? (measure with `getBoundingClientRect().left` — *do not eyeball*)183- Are cards in a grid **same height** (or intentionally different — never accidentally)?184- Are cards in a grid **same width**? (a single card narrower than its peers usually means it didn't fill its grid cell — child needs `width: 100%` or grid template fix)185- Is whitespace distributed (no card stretched into empty band)?186- Does the overall composition have a **clear focal point** (eye anchor)?187- **Section vertical padding symmetry** — the gap above and below the page content should match unless deliberately asymmetric. Measure `frame.getBoundingClientRect()` vs. `firstSection.getBoundingClientRect().top` and `lastSection.getBoundingClientRect().bottom`. A page with `padding-top: 32px / padding-bottom: 16px` reads as "the designer forgot."188189Bad signals:190- One card stretches to match a sibling and gains an empty trailing band → **`align-items: stretch` smell**, see `css-judgment` Example 8191- Two-column grid with one column overflowing → **`min-width: 0` smell**, see `css-judgment` Example 5192- Card narrower than its peers in the same row → **flex/grid child not filling its track** (the `<button>` shrinking to label width is the most common variant — `<button>` defaults to inline-block sizing, so even inside `flex-direction: column` it shrinks to its content; force with `width: 100%` or `align-items: stretch` on the parent)193- Centered headings but left-aligned body inside the same container194- Page top padding ≠ page bottom padding (asymmetric without intent)195196### 4.5 Typography quality — *A List Apart*, *Refactoring UI*197198> "Get the body right first. Everything else is fine-tuning."199200Check (English/Latin):201- Body 16-19px / line-height 1.5-1.7 / max-width 65-75 char202- Headings tighter line-height (1.05-1.2) / slightly negative letter-spacing203- One or two type families max (sans for UI; serif/mono for editorial accents)204205Check (Korean):206- **Run `korean-typography` skill** for the full checklist (Pretendard, line-height 1.7+, word-break: keep-all, 받침 충돌 가드)207208Bad signals:209- Body < 15px (Latin) / < 16px (Korean) — looks small even when "design article said so"210- All text in the same weight (visual monotony)211- Justified text with awkward gaps212- Long URL / 어절 forcing horizontal scroll → missing `overflow-wrap: break-word`213214### 4.6 Image use & composition — *A List Apart*, *Refactoring UI*, editorial photography best practices215216> "An image is a co-author of the headline. If it doesn't argue for the page, it's filler."217218Check:219- **Aspect ratio**: hero ≥ **1.7:1** (16:9 baseline) on desktop, taller crop (4:5 or 3:4) on mobile. Aspect should be a design decision, not a CMS upload accident.220- **Focal point**: subject in the **rule-of-thirds** anchor, not always center. If text overlays the image, the focal point and the text occupy **different thirds**.221- **Text-over-image contrast**: WCAG 4.5:1 against the **actual pixels behind the text** (not the average). Use a dim overlay or vignette if the image is busy.222- **Photographic vs. illustration choice**: editorial moodshot for emotion, illustration for abstract concepts, photography for product. Don't mix all three on one page without a clear hierarchy of voice.223- **Mobile crop survives**: the desktop hero usually crops to mobile with the right 30% lost. Verify the focal point still reads at the mobile crop.224- **Image-to-text ratio per fold**: an entirely-text fold reads as a wall; an entirely-image fold reads as advertising. Aim for **roughly 40-60% image weight** in editorial sections, 0% in dense info sections — but pick deliberately, not by accident.225- **Loading strategy**: hero `loading="eager"` + `fetchpriority="high"`; below-fold `loading="lazy"`. Mis-set lazy on hero = visible pop-in.226227Bad signals:228- Hero is a generic stock photo of laptops/handshakes — no editorial voice229- Text sits awkwardly over a busy region of the image (no overlay, no contrast)230- Every image is the same 16:9 aspect and same focal placement — visual monotony231- Hero focal point gets cropped out on mobile232233### 4.7 Proportion & negative space — *Vanseo Design (golden section)*, *A List Apart (modular typography)*, *IxDF (white space)*234235> "Whitespace is not absence; it is composition. Proportion is not aesthetics; it is structure."236237**Proportion & modular scale:**238- Type sizes drawn from a **modular scale ratio** (1.250 Major Third / 1.333 Perfect Fourth / 1.5 Augmented Fourth / 1.618 Golden Ratio)? Each step is a deliberate multiple, not a guess.239- Body / lede / heading sizes preserve the ratio across breakpoints?240- Hero column-to-side-column width ratio reads as intentional (e.g., 1.618 : 1 for golden, 1.2 : 1 for near-equal, 2 : 1 for emphasis)?241242**Negative space (Micro vs Macro):**243- **Macro whitespace** (between major sections, page margins, around hero) — generous, defines the page's calm244- **Micro whitespace** (between lines, around paragraph, inside cards) — tight enough that grouping is preserved245- **Ratio**: micro ≈ 1/3 of macro. If they're equal, every group blurs into the next.246- **Gestalt proximity check**: items in the same group should be visually closer to each other than to items in a different group.247- **Page edge margins** scale with viewport (e.g., 16-24px mobile / 40-80px desktop / centered max-width ≤ 1200px on ultra-wide).248249Bad signals:250- Type sizes are 15 / 16 / 18 / 22 / 24 / 28 / 36 — no consistent ratio, looks like trial-and-error251- Section spacing == card spacing == paragraph spacing — everything blurs together (Gestalt proximity broken)252- Page margins are 0 — content hugs viewport edge, no breathing room253- Excessive whitespace **inside** cards but cards crammed against each other (proportion inverted)254- Hero columns split 50/50 by default — perfectly symmetric is often the least interesting choice255256### 4.8 Distinctiveness — *frontend-design* skill alignment257258> "Avoid the generic AI aesthetic: centered hero / 3-card grid / purple gradient / 'Get Started' CTA."259260Check:261- Does the screen have a **point of view** (a specific style choice you can name)?262- Or does it look like every other AI-generated landing page?263- Is at least one element doing something **unexpected but intentional** (asymmetry, oversized number, editorial pull-quote, photographic moodshot)?264265Bad signals:266- Three identical cards in a row, each with icon + title + paragraph + "Learn more"267- Hero is centered headline + subhead + gradient button + gradient blob in background268- Purple-to-pink gradient with no other color discipline269270If the screen feels generic, that's not a polish issue — it's a *design* issue. Surface it.271272---273274## 5. 유저 편의성 lenses (6)275276Based on **Nielsen Norman Group's 10 Usability Heuristics** (Jakob Nielsen, 1994 → still definitive), distilled to the six that show up in screenshots:277278### 5.1 Affordance — *Don Norman, "Design of Everyday Things"*279280> "An object's appearance should suggest how it's used."281282Check:283- Is every clickable element **visually distinct** from static text? (color / underline / button shape)284- Do buttons look pressable (filled or outlined, not just text)?285- Is the **primary action** the most prominent thing on the screen?286- Are icons accompanied by labels (unless they're universally recognized — search, close, menu)?287288Bad signals:289- Inline link looks identical to body text290- "Button" is just colored text, no shape291- Two buttons with the same weight ("Cancel" vs "Submit" both gray outline)292293### 5.2 Visibility of system status — *NN/g #1*294295> "The system should always keep users informed about what is going on, through appropriate feedback within reasonable time."296297Check:298- Is loading state visible (skeleton / spinner / progress)?299- Is success/error feedback visible (toast / banner / inline message)?300- Does the URL / breadcrumb / page title reflect where you are?301- Is selected/active state obvious (nav highlight, tab active border)?302303Bad signals:304- Click button → nothing happens for 2s, no feedback305- Form submits silently with no confirmation306- Active nav item indistinguishable from inactive307308### 5.3 Touch target & hit area — *Apple HIG / Material Design*309310> "Touch targets should be at least 44×44 pt (iOS) / 48×48 dp (Android)."311312Check:313- Every interactive element ≥ **44×44 pt** (use DevTools to measure)?314- Adequate spacing between adjacent targets (≥ 8 pt) to prevent mis-taps?315- Are touch targets sized for the **action's importance** (primary CTA bigger than secondary)?316317Bad signals:318- Icon-only buttons at 24×24 — too small for thumbs319- Two checkboxes 4px apart — adjacent taps overlap320321### 5.4 Readability — *A List Apart*, *WCAG 2.2*322323> "If users can't comfortably read your text, nothing else matters."324325Check (overlap with 심미성 4.5):326- Body ≥ 16-17px (한글이면 17px+)327- Contrast ≥ 4.5:1 (or 3:1 for large/heading text)328- Line length 45-75 char (`max-width: ~65ch` rule of thumb)329- Line-height 1.5+ (한글 1.7+)330- Not all-caps for body (only short labels)331332Bad signals:333- Cramped body filling the entire viewport width (95 char per line)334- Gray text on colored card background → low contrast335- Hangul body at 14px — under-sized for the script336337### 5.5 Navigation clarity — *NN/g #6, #4*338339> "Show users what they're looking at, what they can do, and how they can get back."340341Check:342- Is there always a way back? (header logo, breadcrumb, browser back works)343- Is the current section/tab visibly marked?344- Are nav items grouped logically (no surprise ordering)?345- Does the layout follow platform conventions (e.g., bottom nav on mobile, sidebar on desktop)?346347Bad signals:348- Hash routing breaks browser back button349- Tab UI without a clear "current" state350- Floating CTA covers content with no way to dismiss351352### 5.6 Carousel / horizontal scroll — *Apple HIG (Scroll Views), Material (Horizontal scrollers)*353354> "The first item in a horizontal scroller should align with the rest of the page's content edge — and the last item should have the same trailing breathing room as the leading one."355356Check:357- **First card's left edge** = the page content's left edge? (use `getBoundingClientRect().left` on the first card vs. the page frame's content edge)358- **`scroll-padding-inline-start` / `padding-inline-start`** on the scroller? Without it, when a user swipes back, the first card snaps flush to the viewport edge with zero breathing room.359- **Last card's right edge** has the same breathing room as the first card's left? Or is it amputated by `overflow: hidden`?360- **Scroll snap** behaves on slow swipe? Not jumping past the intended card?361362Bad signals:363- First card touches the viewport edge (no leading padding) — most common; very visible on mobile364- Last card's right edge is exactly at viewport edge → user can't tell if there's more365- `overflow-x: auto` without `scroll-padding` → snap brings cards flush to edge366- Different padding on left vs. right of the strip367368Fix recipe (CSS):369```css370.carousel {371 display: flex;372 gap: var(--space-md);373 overflow-x: auto;374 scroll-snap-type: x mandatory;375 /* leading / trailing breathing room */376 padding-inline: var(--space-lg);377 /* preserve breathing room on snap-back */378 scroll-padding-inline: var(--space-lg);379}380.carousel > * { scroll-snap-align: start; }381```382383### 5.7 Micro-interaction & feedback — *Apple HIG (Animation), NN/g #1*384385> "A polished UI has motion. Not flashy motion — confirmation motion."386387Check:388- Every interactive element has a **visible state change** on hover (desktop) and active/press (mobile)? (subtle color shift, scale 0.97 on press, border emphasis — pick a small, consistent vocabulary)389- Cards / buttons feel "alive" on hover, or are they static rectangles?390- **Critical feedback motions are present**: progress bar fill animation, save toast slide-in, optimistic UI update, skeleton shimmer391- **Restraint**: motion ≤ 200ms (NN/g), `prefers-reduced-motion` respected, no decorative loops competing with content392393Bad signals:394- Buttons are visually inert until clicked (no hover, no press) — feels unresponsive395- All motion happens in 800ms eases — feels sluggish396- Decorative parallax / scroll-jacking on a content site — distracting397- `:hover` defined but no `:focus-visible` — keyboard users get nothing398399Patterns to add (light, default these in):400```css401.interactiveCard {402 border: 1px solid var(--border-card);403 transition:404 transform 0.12s ease,405 background-color 0.12s ease,406 border-color 0.12s ease,407 border-width 0.12s ease;408}409.interactiveCard:hover {410 background-color: var(--surface-hover);411 /* Hover border 강화: 두꺼워지고 theme color로 — 카드가 '집어지는' 느낌 */412 border: 2px solid var(--accent-brand);413 /* 두께가 1→2px로 늘면 1px만큼 contents가 밀려나므로 padding을 1px 줄여 보정 */414 padding: calc(var(--space-md) - 1px);415}416.interactiveCard:active { transform: scale(0.98); }417.interactiveCard:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: 2px; }418@media (prefers-reduced-motion: reduce) {419 .interactiveCard { transition: none; }420 .interactiveCard:active { transform: none; }421}422```423424Hover의 visual weight 변화는 두 가지 같이 가야 자연스럽다:425- **color shift**(미세한 배경 톤 변화)426- **border 강화**(두께 +1px + theme accent color)427428color shift만 있으면 평평하고, border만 두꺼워지면 보더가 갑자기 튀어나온다. 둘이 동시에 변할 때 카드가 "들려" 보인다.429430### 5.8 Empty / error / edge states — *NN/g #9*431432> "Help users recognize, diagnose, and recover from errors."433434Check:435- Empty state has a **prompt** ("아직 데이터가 없어요 → 첫 항목 만들기"), not a blank page436- Error state explains what went wrong + what to do (not "Error 500")437- Loading state doesn't block the entire UI if avoidable (skeleton over spinner)438- 404 / unauthorized has a path back439440Bad signals:441- Empty list with no message442- "Something went wrong" with no detail or retry443- Full-screen spinner for every action444445---446447## 6. Audit procedure448449For each captured screenshot:4504511. **First impression (10s)** — Note your gut reaction in one sentence. *"What do I see first? What feels off?"* This single sentence often surfaces the real issue faster than checklists.4522. **Walk the 16 lenses** — Don't skip ones that feel "obviously fine." The skip is where regressions hide.4533. **Measure — don't eyeball.** This is the single biggest lift in audit quality. Open the page in cmux/playwright and pull numbers, then compare to thresholds:454455 ```js456 // Page frame / canvas457 const frame = document.querySelector('section[aria-label="..."]');458 frame.getBoundingClientRect();459460 // Every card in a row — check left/width parity461 const cards = document.querySelectorAll('[role="listitem"]');462 Array.from(cards).map(c => {463 const r = c.getBoundingClientRect();464 return { top: r.top, left: r.left, width: r.width, height: r.height };465 });466467 // Vertical padding symmetry on the page468 const first = document.querySelector('section:first-of-type');469 const last = document.querySelector('section:last-of-type');470 ({ top: first.getBoundingClientRect().top, bottomGap: innerHeight - last.getBoundingClientRect().bottom });471472 // Touch target sizes473 Array.from(document.querySelectorAll('button, a, [role="button"]')).map(el => {474 const r = el.getBoundingClientRect();475 return { label: el.textContent?.slice(0,20), w: r.width, h: r.height };476 }).filter(b => b.w < 44 || b.h < 44);477478 // Contrast (rough — use DevTools "Contrast" for the official measurement)479 getComputedStyle(document.querySelector('p')).color;480 ```481482 Patterns to compare against:483 - All cards in a row → `width` should be equal (±1px). Unequal means a child didn't fill its grid cell.484 - All cards in a row → `left` should be equal (or in monotonic carousel order).485 - Vertical padding symmetry → top gap vs. bottom gap within 4px.486 - Touch targets < 44×44 → flag.487 - Body text contrast < 4.5:1 → flag.4884894. **Note severity per finding**:490 - **blocker** — fails WCAG, breaks usability, unreadable text, broken layout (overflow / cropping)491 - **major** — clear UX issue but doesn't break the screen (low contrast, missing feedback, ambiguous CTA)492 - **minor** — style inconsistency, slightly off rhythm, could be more distinctive493 - **nit** — pure preference, ignore unless many accumulate4945. **For each finding** include: severity / axis / lens / location (component or screenshot region) / **measured numbers** / recommended fix / reference (which article / heuristic).495496> "정렬이 안 맞아 보여" 는 약함. "Card 3의 width가 156px인데 나머지 4개는 168px — `<button>`이 flex column 안에서 콘텐츠 폭으로 줄어듦. `width: 100%` 추가." 가 강함.497498### 6.1 Self-critique loop (강제)499500스크린샷을 본 직후, 다음 한 줄을 **반드시** 적는다 — "approve 직전 마지막 솔직한 평가":501502> *"내가 이 화면을 처음 보는 사람이라면, '이 부분은 디자인이 좀 약하다'고 말할 곳은 어디인가?"*503504찾았다면 그 위치를 **major 이상**으로 분류한다. 자가 critique 없이 "다 괜찮다"로 끝나는 audit은 lens를 walk했더라도 실패한 audit이다. 이 한 줄은 보통 디자인 자체의 결함(레이아웃 결정 / 카드 컨셉 / 정보 위계)을 노출하며, 그건 css-judgment로는 못 잡고 **재설계로만** 잡힌다.505506재설계가 필요하다고 판단되면 해당 컴포넌트는 `frontend-design` 스킬로 라우팅하고 "redesign 후보" 로 보고에 명시.507508---509510## 7. Report format511512```513# Visual Design Audit — <screen / route>514515## First impression516<one sentence>517518## Findings (sorted by severity)519520### blocker — <axis> — <lens>521- Location: <component path or screenshot annotation>522- Issue: <what's wrong, measured if possible>523- Fix: <specific css/component change>524- Ref: <article / heuristic / `css-judgment` Example N>525526### major — ...527528### minor — ...529530## Passed lenses531- <lens>: ✓ <why it's good — short>532- ...533534## Outstanding535<questions, scope creep risks, things needing user input>536```537538Keep findings **specific and measurable**. "여백이 너무 많다" 는 약함. "heroCard 하단에 240px 빈 band, `align-items: stretch` 결과 — `align-self: start`로 fix" 는 강함.539540---541542## 8. Anti-patterns (audit itself)543544- ❌ "Looks good to me" — that's not an audit. Walk the lenses.545- ❌ Skipping mobile capture because "desktop looks fine"546- ❌ Approving without measuring contrast / size when the issue is on that lens547- ❌ Lumping all findings as "minor"548- ❌ Suggesting fixes outside the screen (refactor X, rebuild Y) — stay on what the screenshot shows549- ❌ Writing the report without the screenshots inline / referenced550- ❌ **Screenshotting without verifying scroll position succeeded** — `cmux browser scroll --y 0` silently no-ops if the page uses an inner scroll container. Always re-read `scrollTop` after a scroll command. See §2-A / §2-B.551- ❌ **Eyeballing alignment / width / padding parity** — these are number questions. Use `getBoundingClientRect()`; report exact px deltas. "Looks off" is not a finding.552- ❌ **Drawing missing icons by hand inside the audit** — if a screenshot reveals empty/broken icon slots or fake-photo SVGs, route through `image-asset-strategy` → `/codex:rescue` (Codex owns `imagegen`, can run with `--write` to land files directly). Do not "just sketch a placeholder."553- ❌ **Stopping at the first finding** — full-pass the lenses even if you find a blocker early. Subsequent findings often share a root cause and fix together.554555---556557## 9. Relation to sibling skills558559| Skill | Relation |560|---|---|561| `visual-reference-compare` | Comparison against a **supplied reference image**. Run this skill **in addition** when you have a reference, not instead. |562| `css-judgment` | Where most of the recommended fixes will land. The audit *finds* issues; css-judgment *guides the fix*. Example 5 / 8 / 9 are common targets. |563| `korean-typography` | When body is Hangul, defer the typography lens (4.5 / 5.4) to its checklist. |564| `image-asset-strategy` | If the audit finds a fake SVG face / product / hero, route it back through asset-strategy → Codex / `imagegen`. |565| `frontend-design` | Distinctiveness lens (4.6) — if audit flags "looks AI-generic," the next iteration goes through frontend-design's "avoid generic aesthetic" guide. |566| NN/g 10 Heuristics | https://www.nngroup.com/articles/ten-usability-heuristics/ — the canonical reference for §5 lenses. |567| WCAG 2.2 | https://www.w3.org/TR/WCAG22/ — color contrast and target size compliance. |568| Apple HIG / Material Design | Platform conventions for touch target, navigation, gestures. |569| Refactoring UI | https://www.refactoringui.com/ — the canonical reference for §4 lenses. |570571---572573## 10. When the audit blocks ship574575If you find ≥ 1 **blocker**, do not approve. Either:576- Fix it now (route to `css-judgment` / `korean-typography` / etc.)577- Or report it clearly and ask the user before shipping578579If you find only **major / minor / nit**, list them in the punch list and let the user decide what to ship-now vs. file-for-later.580581---582583## 11. References (one-line each)584585- **Refactoring UI** — Wathan & Schoger. Visual hierarchy, spacing, color, typography. Highest density of actionable advice.586- **NN/g 10 Usability Heuristics** — Nielsen. The default usability vocabulary.587- **The Design of Everyday Things** — Norman. Affordance, signifier, feedback.588- **A List Apart** — typography, readability essays (Tim Brown's "More Meaningful Typography" etc.).589- **WCAG 2.2** — color contrast, text size, target size compliance.590- **Apple Human Interface Guidelines** — touch target, platform conventions.591- **Material Design 3 Guidelines** — touch target, motion, accessibility.592- **Every Layout** — Bell & Andrew. CSS layout primitives and rhythm.593- **Tufte, Edward** — *Visual Display of Quantitative Information*. Density, signal/noise.594- **8-Point Grid (Bryn Jackson, Spec.fm)** — multiples-of-8 spacing scale + 4pt baseline grid for typography.595- **Modular scale (Tim Brown / A List Apart)** — type sizes drawn from a ratio (1.250 / 1.333 / 1.5 / 1.618).596- **Golden ratio in UI (Figma resource library, NN/g)** — 1.618 as a column / typography / button proportion.597- **Negative space in design (IxDF)** — micro vs macro whitespace; micro ≈ 1/3 of macro; Gestalt proximity for grouping.598- **Compositional balance (Smashing Magazine)** — symmetric vs. asymmetric balance; visual tension via weight contrast and leading lines.599- **Hero image best practices (Shopify / Squarespace blog)** — 16:9 baseline, mobile crop survival, focal at rule-of-thirds, text/image contrast.600- **한국 디자인 블로그** — toss / 카카오디자인 / 우아한형제들 / karrot — Hangul-aware patterns to compare against.