UI Audit Method
This skill does not tell you what good UI looks like. It tells you how to measure it, how to verify what you measured, and how to record verdicts so they stick. What counts as a problem is the project's call.
One principle: never report an impression. "This looks kind of AI-generated"
is not a finding. rounded-xl, 25 occurrences is.
Two failure modes
Read this before starting. There are only two ways this work goes wrong.
- Inventing problems. Flagging clean metrics and manufacturing work. A metric sitting at zero is not a blank to fill — it is a state to preserve.
- Forgetting settled verdicts. An item closed last time as "checked, not a problem" comes back next audit. That is why verdicts go in a file.
The baseline comes first
This skill is half a tool on its own. Without knowing what is normal for the project, you produce numbers with nothing to compare them against.
<project>/.claude/ui-baseline.md
If it does not exist, copy references/BASELINE-TEMPLATE.md and fill it in.
Four things live there:
- What this app is — web or desktop, fixed or fluid viewport, how many themes
- What must not be touched — palette tokens, framework idioms, existing rules
- Metrics — what this project decided to count, why each one matters here, and the values at audit time versus now. The "now" column is the regression line.
- Verdicts — items closed as "not a problem", the evidence, and the condition that would reopen them
Choosing metrics is the project's job. This skill supplies the counting.
1. Establish what you are scanning
Decide what the commands read before you run them. UI lives in different extensions per framework.
for e in svelte vue jsx tsx astro html css scss; do
n=$(git ls-files "*.$e" | wc -l); [ "$n" -gt 0 ] && echo " .$e $n"
done
Examples below use *.svelte. Adjust --include to match.
2. Counting — and five pitfalls
Whatever the metric, these traps apply. Every one produced a wrong number in real use.
Pitfall 1 — counting a subset and believing it is the whole
Filtering by size or shape silently drops other forms of the same problem.
grep -rn "text-[45]xl" src --include=*.svelte # large ones only -> 9
# exhaustive scan -> 85. Everything embedded in button labels and status
# badges was missing from the first number.
Count exhaustively first, classify second. Never let the filter define the population.
Pitfall 2 — the regex eats template syntax
grep -rn "#[0-9a-fA-F]\{3,8\}" src --include=*.svelte # 14 (false)
grep -rn "#[0-9a-fA-F]\{3,8\}\b" src --include=*.svelte # 2 (real)
Without \b, Svelte's {#each} matches as #eac. Vue's #default and JSX
#region comments leak the same way. State the boundary.
Pitfall 3 — scraping values that are legitimate
grep -rn "\[[0-9]*px\]" src --include=*.svelte # 11 — includes layout sizes
grep -rn "text-\[[0-9]*px\]" src --include=*.svelte # 7 — fonts only
w-[260px] is correct in a fixed-size app. If you do not know what is normal
you are counting noise, not a metric. Hence the baseline first.
Pitfall 4 — missing the bare form
grep -rho "rounded-[a-z0-9]*" src --include=*.svelte | sort -u # 4 kinds
grep -rhoE "rounded(-[a-z0-9]+)?" src --include=*.svelte | sort -u # 5 kinds
Catching suffix-less forms (rounded, border, shadow) needs -E and ?.
Pitfall 5 — summing modifier variants
grep -rhoE "bg-accent/?[0-9]*" src --include=*.svelte | grep -c '^bg-accent$'
Counting bg-accent/80 and friends turns 21 into 39. Hover-only classes are
the same story — folding something invisible at rest into an exposure count
manufactures an overuse finding.
3. Verify by rendering (only when layout or color is in question)
If you are only counting class names, skip this. When layout is suspect or you
need to know whether a color actually applies, follow
references/RENDER-VERIFICATION.md.
One rule from there matters enough to state up front — never argue a layout bug from a screenshot. On a display scaled above 100%, capturing at the window's logical size crops the right edge and manufactures an overflow that is not there. This has produced a false report in practice.
When a screenshot and a measurement disagree, believe the measurement. If the same code is fine in a browser but looks broken in the app, suspect the observation, not the code.
4. Report
Give file:line and the replacement code. Never write "consider improving".
src/components/Card.tsx:63 transition-all
-> only colors change here. transition-colors
Compare against the baseline's "now" column and say whether this is better or worse. A list of absolute numbers gives the reader nothing to judge.
5. Fix
Do not edit without approval. Wait for an explicit "fix it".
One category per commit. Radius work is radius only; transition work is transition only. Mixed commits cannot be reverted.
Always check whether a blanket replacement is safe — the same class often
appears for different reasons. In one case 5 of 12 transition-all hits were
progress bars whose width genuinely animates; a blanket rewrite to
transition-colors would have silently deleted the animation.
Run the project's type check and build afterward, then update the "now" column. That is the next audit's reference line.
6. Record the verdict
Items closed as "checked, not a problem" go in the baseline with evidence and a reopen condition. Otherwise they come back next time.
| accent usage | 77 | not a problem | reopen if opaque bg-accent exceeds 21 |
Evidence is numeric. Not "looks fine" but "27 of 77 are hover-only, so resting exposure is 50".
When nothing is wrong
Say so briefly and stop. Remember failure mode 1.