
Design Review
- 83 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
design-review is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
Key points
- design-review
- AI & Agent Building
- AI-coding skill
Design Review by the numbers
- 83 all-time installs (skills.sh)
- +8 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,111 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill design-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 83 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I helps with ai & agent building tasks during ai-assisted development?
Helps with ai & agent building tasks during AI-assisted development.
Who is it for?
Best when you're working on ai & agent building and need structured help with design-review.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks during ai-assisted development, or when design-review is a claude code skill for ai & agent building. it helps solo builders move faster with ai-assisted coding.
What you get
Structured output aligned to design-review: design-review; AI & Agent Building; AI-coding skill.
Files
Design Review
Conduct a design review of UI code and return a prioritised critique. The reviewer's lens is Emil Kowalski's design-engineering philosophy — taste is the differentiator; the unseen details compound; show the eye where to look — made concrete with the heuristics from Refactoring UI, WCAG, and MDN.
This is a read-only review skill: it diagnoses and proposes fixes; it does not rewrite the codebase. Each finding names the wrong default the code fell into, the exact fix, and why it matters.
When to Apply
- The user asks to "review this UI", run a "design review", or "critique" a component, screen, or page.
- The user says the output "looks off", "looks AI-generated", "looks like a wireframe", or "feels generic", and wants to know why.
- A PR touches CSS/JSX/Tailwind and the user wants design feedback before merge.
- The user wants to raise the visual polish or accessibility of an existing interface.
Not for building UI from scratch (use web-taste) or for the exhaustive animation rule set (use emilkowal-animations).
How to Run the Review
Two modes. A static review reads the code and is the default. A runtime review additionally drives a real browser to measure what the code can't show — animation timing and dropped frames, layout shift, the live focus order and accessibility tree, and the multi-page flow clicked through end to end. Switch to runtime whenever the verdict turns on rendered behaviour (the motion-, interact-, and flow- categories), per runtime-capture.md.
1. Orient — the 0.5-second test. Before reading line by line, picture the rendered screen. Where does the eye land first? Is there a single focal point, or does everything carry equal weight? This frames which categories matter most for this UI. 2. Pass the categories in priority order (table below). For each decision the code makes, read the matching reference file and check the code against it. Visual hierarchy and spacing are where the largest, most frequent problems live — start there. 3. For multi-page or interaction-driven UX, walk it in a browser. When the brief is a flow ("review this onboarding") or the issue is felt in motion (jank, blank route flashes, lost focus), capture runtime evidence per runtime-capture.md so the Before column is a measured value, not a guess. 4. Record each problem as a finding with a Before (the exact code or measurement), an After (the concrete fix), a Why (the principle), and a context-assigned Severity. 5. Close with a verdict: the top 3 fixes, ranked by impact, so the author knows what to change first.
Output Format (Required)
Report findings as a single markdown table, one row per issue. Do not write findings as prose or as Before: / After: on separate lines.
| Severity | Before | After | Why |
|---|---|---|---|
| High | transition: all 300ms ease-in | transition: opacity 180ms cubic-bezier(0.23, 1, 0.32, 1) | ease-in feels sluggish on entry; name the property and use a strong ease-out curve |
| High | every button bg-indigo-600 | one filled primary; others ghost/outline | Equal-weight buttons compete; one primary makes the next step obvious |
| Medium | color: #000 on #fff | color: hsl(222 47% 11%) | Pure black is harsher than ink and reads as stark |
| Critical | <div onClick={remove}> | <button type="button" onClick={remove}> | A div is unreachable by keyboard and invisible to screen readers |
Wrong format — never do this:
Before: transition: all 300ms
After: transition: opacity 180ms ease-out
────────────────────────────
Before: color #000
After: color slate-900Severity guide (assigned per finding, by impact in this UI):
| Severity | Meaning |
|---|---|
| Critical | Breaks usability or accessibility — fails contrast, no keyboard access, unreadable text |
| High | Clearly damages the design — no hierarchy, cramped spacing, competing primary actions |
| Medium | Noticeable polish gap — default easing, uniform line-height, missing press feedback |
| Low | Minor refinement — a value slightly off the scale |
Finish with: Top 3 fixes — the highest-impact rows, in the order the author should tackle them.
Rule Categories
| # | Category | Prefix | Covers |
|---|---|---|---|
| 1 | Visual Hierarchy | hier- | Focal point, emphasis technique, one primary action, value-over-label, space over borders |
| 2 | Spacing & Layout | space- | Spacing scale, generous whitespace, proximity grouping, constrained width |
| 3 | Typography | type- | Type scale, line length, line-height, alignment, readable body text |
| 4 | Colour & Contrast | color- | Near-black text, WCAG contrast, HSL ramps, restrained accents, colour-plus-cue |
| 5 | Component States & Feedback | state- | Press feedback, focus-visible, the full state matrix, empty states |
| 6 | Motion & Animation | motion- | Purpose/frequency, ease-out curves, sub-300ms, enter origin/scale, transform-only |
| 7 | Responsiveness & Touch | resp- | Fluid mobile-first, 44px targets, gating hover |
| 8 | Accessibility & Semantics | access- | Semantic elements, accessible names, reduced-motion |
| 9 | Flow & Navigation | flow- | App-shell consistency, view-state persistence, entry-point integrity, wayfinding |
| 10 | Interaction Continuity | interact- | Bridging route transitions, async feedback, focus on navigation |
Quick Reference
1. Visual Hierarchy (hier-)
- `hier-one-focal-point` — Establish one clear focal point per screen
- `hier-emphasis-color-weight` — Use colour and weight to set emphasis, not size alone
- `hier-one-primary-action` — Limit each view to one primary action
- `hier-values-over-labels` — Make values louder than their labels
- `hier-replace-borders-with-space` — Replace borders with spacing and background
2. Spacing & Layout (space-)
- `space-use-a-scale` — Size spacing from a consistent scale
- `space-start-generous` — Give layouts more whitespace than feels necessary
- `space-proximity-groups` — Vary spacing to show what is grouped
- `space-constrain-measure` — Cap and centre the page container width
3. Typography (type-)
- `type-modular-scale` — Choose font sizes from a small type scale
- `type-limit-line-length` — Limit body line length for readability
- `type-line-height-by-size` — Set line-height relative to font size
- `type-left-align-prose` — Align multi-line text to the left
- `type-readable-body-size` — Keep body text large and solid enough to read
4. Colour & Contrast (color-)
- `color-avoid-pure-black` — Use a near-black instead of pure black
- `color-meet-contrast` — Meet WCAG contrast for body text
- `color-hsl-scales` — Define colour as HSL shade ramps
- `color-limit-accents` — Limit the palette to one accent plus neutrals
- `color-not-only-signal` — Pair colour with a second cue for state
5. Component States & Feedback (state-)
- `state-press-feedback` — Give pressable elements active feedback
- `state-focus-visible` — Keep an accessible focus indicator
- `state-design-all-states` — Design every interactive state, not just the default
- `state-empty-state` — Design the empty state with guidance
6. Motion & Animation (motion-)
- `motion-needs-purpose` — Animate only with a purpose
- `motion-ease-out-custom` — Use ease-out with a custom curve for UI transitions
- `motion-under-300ms` — Keep UI transitions under 300ms
- `motion-enter-origin-scale` — Enter from a near scale and the trigger's origin
- `motion-transform-opacity-only` — Animate only transform and opacity
For drag, gestures, springs, stagger, clip-path, and the full timing/easing tables, defer to the emilkowal-animations skill.
7. Responsiveness & Touch (resp-)
- `resp-fluid-not-fixed` — Build mobile-first with fluid widths
- `resp-touch-target-size` — Size touch targets to at least 44px
- `resp-gate-hover` — Gate hover-only affordances behind a pointer query
8. Accessibility & Semantics (access-)
- `access-semantic-elements` — Use semantic elements for interactive controls
- `access-name-icon-controls` — Give icon-only controls an accessible name
- `access-respect-reduced-motion` — Honor the reduced-motion preference
9. Flow & Navigation (flow-)
Reviews the experience across pages, which single-screen review can't see. Walk the flow in a browser (runtime-capture.md).
- `flow-consistent-shell` — Keep the app shell consistent across pages
- `flow-preserve-state-on-nav` — Preserve scroll and view state across navigation
- `flow-entry-point-integrity` — Make every page work as a first entry point
- `flow-wayfinding` — Show where the user is and the way back
10. Interaction Continuity (interact-)
Reviews whether the experience stays continuous over time and across transitions — the dimension a screenshot can't show. Best judged against a captured trace (runtime-capture.md).
- `interact-bridge-route-transitions` — Bridge route changes so the screen never flashes blank
- `interact-feedback-spans-async` — Fill the gap while an interaction is in flight
- `interact-move-focus-on-navigation` — Move focus to new content after client-side navigation
How to Use
Read a reference file when its decision comes up in the code under review. Each rule names the wrong default it corrects, then shows the canonical fix (with an Incorrect/Correct contrast only where the wrong way is a real trap). Cite the rule slug in the "Why" column so the author can follow up.
- Runtime capture — drive a real browser (chrome-devtools-mcp) to measure motion, jank, focus order, and multi-page flow when a static read isn't enough
- Section definitions — category structure and order
- Rule template — for adding new rules
- AGENTS.md — auto-built table of contents across all rules
Related Skills
emilkowal-animations— the exhaustive animation rule set (easing, gestures, springs, stagger); this skill defers motion depth to it.web-taste— building React/Next/Tailwind UI with taste from the ground up (the build counterpart to this review).tailwind-ui-refactor— applying these fixes as Tailwind refactors.ui-design— broader build-time frontend reference (Core Web Vitals, forms, dark mode). Where the two overlap (semantics, contrast, focus, single primary action), reach forui-designwhile authoring anddesign-reviewwhile reviewing.
Reference Files
| File | Description |
|---|---|
| references/_runtime-capture.md | Browser-driven capture playbook (chrome-devtools-mcp via mcporter) |
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and source references |
UI Design Review (React / CSS / Tailwind)
Version 0.2.0 dot-skills May 2026
---
Abstract
A design review for UI that reports findings as a prioritised Before/After/Why table across visual hierarchy, spacing, typography, colour and contrast, component states, motion, responsiveness, accessibility, multi-page flow and navigation, and interaction continuity — grounded in Refactoring UI and Emil Kowalski's design-engineering principles. Reviews static code (React/JSX, CSS, Tailwind) and, when the verdict turns on rendered behaviour, drives a real browser via chrome-devtools-mcp to measure animation timing, jank/FPS, focus order, the accessibility tree, and the multi-page flow that a screenshot cannot show.
---
Table of Contents
1. Visual Hierarchy
- 1.1 Establish one clear focal point per screen
- 1.2 Limit each view to one primary action
- 1.3 Make values louder than their labels
- 1.4 Replace borders with spacing and background
- 1.5 Use colour and weight to set emphasis, not size alone
2. Spacing & Layout
- 2.1 Cap and centre the page container width
- 2.2 Give layouts more whitespace than feels necessary
- 2.3 Size spacing from a consistent scale
- 2.4 Vary spacing to show what is grouped
3. Typography
- 3.1 Align multi-line text to the left
- 3.2 Choose font sizes from a small type scale
- 3.3 Keep body text large and solid enough to read
- 3.4 Limit body line length for readability
- 3.5 Set line-height relative to font size
4. Color & Contrast
- 4.1 Define colour as HSL shade ramps
- 4.2 Limit the palette to one accent plus neutrals
- 4.3 Meet WCAG contrast for body text
- 4.4 Pair colour with a second cue for state
- 4.5 Use a near-black instead of pure black
5. Component States & Feedback
- 5.1 Design every interactive state, not just the default
- 5.2 Design the empty state with guidance
- 5.3 Give pressable elements active feedback
- 5.4 Keep an accessible focus indicator
6. Motion & Animation
- 6.1 Animate only transform and opacity
- 6.2 Animate only with a purpose
- 6.3 Enter from a near scale and the trigger's origin
- 6.4 Keep UI transitions under 300ms
- 6.5 Use ease-out with a custom curve for UI transitions
7. Responsiveness & Touch
- 7.1 Build mobile-first with fluid widths
- 7.2 Gate hover-only affordances behind a pointer query
- 7.3 Size touch targets to at least 44px
8. Accessibility & Semantics
- 8.1 Give icon-only controls an accessible name
- 8.2 Honor the reduced-motion preference
- 8.3 Use semantic elements for interactive controls
9. Flow & Navigation
- 9.1 Keep the app shell consistent across pages
- 9.2 Make every page work as a first entry point
- 9.3 Preserve scroll and view state across navigation
- 9.4 Show where the user is and the way back
10. Interaction Continuity
- 10.1 Bridge route changes so the screen never flashes blank
- 10.2 Fill the gap while an interaction is in flight
- 10.3 Move focus to new content after client-side navigation
---
References
1. https://www.refactoringui.com/ 2. https://medium.com/refactoring-ui/7-practical-tips-for-cheating-at-design-40c736799886 3. https://emilkowal.ski/ui/great-animations 4. https://emilkowal.ski/ui/good-vs-great-animations 5. https://emilkowal.ski/ui/you-dont-need-animations 6. https://practicaltypography.com/ 7. https://www.w3.org/WAI/WCAG22/ 8. https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible 9. https://developer.apple.com/design/human-interface-guidelines/layout 10. https://www.nngroup.com/articles/ten-usability-heuristics/ 11. https://www.nngroup.com/articles/breadcrumbs/ 12. https://www.nngroup.com/articles/response-times-3-important-limits/ 13. https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration 14. https://developer.mozilla.org/en-US/docs/Web/API/History_API 15. https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API 16. https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/ 17. https://github.com/ChromeDevTools/chrome-devtools-mcp 18. https://github.com/steipete/agent-scripts/blob/main/skills/browser-use/mcporter-config.md
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for creating new rules |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
Rule Title Here
Name the wrong default this rule corrects and its concrete consequence, in 1-3 sentences. Explain the why — the reviewer (and the author) generalises from the reason, not the instruction. Do not restate something a competent author already does correctly.
/* The canonical fix. Real, domain-realistic class/component names — not foo/bar. */
.invoice-summary { color: hsl(222 47% 11%); }Reference: Source title
<!-- Add an Incorrect (…): / Correct (…): pair ONLY when the wrong way is a genuine, common trap. Keep the diff minimal (same names, only the key line changes). A strawman foil is worse than a single good example.
This is a taste/correctness skill, not a performance one — do NOT add impact: / impactDescription: frontmatter. Severity is assigned per finding at review time, in the output table, not baked into the rule. -->
{
"name": "design-review",
"version": "0.2.0",
"organization": "dot-skills",
"technology": "UI Design Review (React / CSS / Tailwind)",
"discipline": "distillation",
"type": "code-quality",
"date": "May 2026",
"abstract": "A design review for UI that reports findings as a prioritised Before/After/Why table across visual hierarchy, spacing, typography, colour and contrast, component states, motion, responsiveness, accessibility, multi-page flow and navigation, and interaction continuity — grounded in Refactoring UI and Emil Kowalski's design-engineering principles. Reviews static code (React/JSX, CSS, Tailwind) and, when the verdict turns on rendered behaviour, drives a real browser via chrome-devtools-mcp to measure animation timing, jank/FPS, focus order, the accessibility tree, and the multi-page flow that a screenshot cannot show.",
"references": [
"https://www.refactoringui.com/",
"https://medium.com/refactoring-ui/7-practical-tips-for-cheating-at-design-40c736799886",
"https://emilkowal.ski/ui/great-animations",
"https://emilkowal.ski/ui/good-vs-great-animations",
"https://emilkowal.ski/ui/you-dont-need-animations",
"https://practicaltypography.com/",
"https://www.w3.org/WAI/WCAG22/",
"https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible",
"https://developer.apple.com/design/human-interface-guidelines/layout",
"https://www.nngroup.com/articles/ten-usability-heuristics/",
"https://www.nngroup.com/articles/breadcrumbs/",
"https://www.nngroup.com/articles/response-times-3-important-limits/",
"https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration",
"https://developer.mozilla.org/en-US/docs/Web/API/History_API",
"https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API",
"https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/",
"https://github.com/ChromeDevTools/chrome-devtools-mcp",
"https://github.com/steipete/agent-scripts/blob/main/skills/browser-use/mcporter-config.md"
]
}
Runtime Capture — reviewing the running UI, not just the code
A static read of CSS/JSX tells you what the code intends; it can't tell you what the browser actually renders. A screenshot is a single frozen frame. Some of the most common design defects only exist in motion and across time:
- the easing/duration the code declares vs. what runs after the framework, GPU, and main-thread contention have their say;
- dropped frames and jank during a transition (a screenshot is always 60fps);
- layout shift (CLS) as content streams in;
- the real focus order and the live accessibility tree as a user moves through the app;
- whether a multi-page flow holds together when you actually click through it.
When the review touches the motion-, interact-, flow-, state-, or access- categories and the verdict depends on rendered behaviour, drive a real browser and capture the evidence. This skill stays read-only: you observe the running app, you do not modify it.
Tooling
Capture runs through `chrome-devtools-mcp` (Chrome's DevTools exposed as MCP tools), invoked from the CLI with `mcporter`. Configure it once per the mcporter Chrome config; the default chrome-devtools server reattaches to your existing Chrome profile (logged-in state intact). Use the chrome-isolated server only when you need a clean, signed-out session.
Confirm the exact tool arguments before relying on them — they evolve with the package:
mcporter list chrome-devtools --schema # authoritative arg shapes for every tool
mcporter call chrome-devtools.list_pages --args '{}' --output text # smoke test
mcporter daemon restart # if calls hang or the page list is staleThe thin browser-use SKILL wraps only snapshot/navigate/click/fill/evaluate. Thericher introspection below (performance traces, animation timing, a11y tree) comes from
the underlying chrome-devtools-mcp tools — call them directly.What to capture, by review category
Walk the multi-page flow → flow-
Click through the real journey and snapshot each step; this is the only way to see the app shell drift, lost scroll/filters, and broken entry points that single-screen review misses.
mcporter call chrome-devtools.list_pages --args '{}' --output text
mcporter call chrome-devtools.select_page --args '{"pageId":1}' --output text
mcporter call chrome-devtools.navigate_page --args '{"type":"url","url":"https://app.local/orders"}' --output text
mcporter call chrome-devtools.take_snapshot --args '{}' --output text # a11y-tree snapshot of page 1
mcporter call chrome-devtools.click --args '{"uid":"1_42","includeSnapshot":true}' --output text
mcporter call chrome-devtools.navigate_page --args '{"type":"back"}' --output text # test Back: is scroll/filter state restored?Open a route directly in a fresh tab to test entry-point integrity (flow-entry-point-integrity):
mcporter call chrome-devtools.new_page --args '{"url":"https://app.local/orders/8231"}' --output text
mcporter call chrome-devtools.take_snapshot --args '{}' --output text # does a deep link render, or break?Animation timeline + jank + CLS → motion-, interact-
Record a trace around the interaction, then read the insights for long tasks, dropped frames, and layout shift — the measured truth behind motion-under-300ms and interact-bridge-route-transitions.
mcporter call chrome-devtools.performance_start_trace --args '{"reload":false}' --output text
mcporter call chrome-devtools.click --args '{"uid":"1_42"}' --output text # trigger the transition under trace
mcporter call chrome-devtools.performance_stop_trace --args '{}' --output json
mcporter call chrome-devtools.performance_analyze_insight --args '{"insightName":"CLSCulprits"}' --output jsonReal frame rate + computed motion → motion-, interact-
evaluate_script runs JS in the page, so you can read what actually animated and sample the frame rate during a second of interaction:
# What is actually animating, and with what timing the browser resolved?
mcporter call chrome-devtools.evaluate_script --output json --args '{"function":"() => document.getAnimations().map(a => ({ name: a.animationName || a.transitionProperty, duration: a.effect.getComputedTiming().duration, easing: a.effect.getComputedTiming().easing }))"}'
# Sample FPS and accumulated layout shift over ~1s (run, then trigger the interaction)
mcporter call chrome-devtools.evaluate_script --output json --args '{"function":"async () => { const shifts=[]; const po=new PerformanceObserver(l=>{for(const e of l.getEntries()) if(!e.hadRecentInput) shifts.push(e.value);}); po.observe({type:\"layout-shift\",buffered:true}); let f=0; const t0=performance.now(); await new Promise(r=>{const tick=()=>{f++; performance.now()-t0<1000?requestAnimationFrame(tick):r();}; requestAnimationFrame(tick);}); po.disconnect(); return { fps:f, cls:+shifts.reduce((a,b)=>a+b,0).toFixed(4) }; }"}'Accessibility tree + focus order → access-, interact-
The verbose snapshot is the a11y tree as assistive tech sees it. Read activeElement before and after a navigation to confirm focus moves to new content (interact-move-focus-on-navigation):
mcporter call chrome-devtools.take_snapshot --args '{"verbose":true}' --output text
mcporter call chrome-devtools.evaluate_script --output json --args '{"function":"() => ({ tag: document.activeElement?.tagName, label: document.activeElement?.textContent?.trim().slice(0,40) })"}'Under throttle and at breakpoints → resp-, motion-
Motion that's smooth on your machine janks on a mid-tier phone. Emulate the constraint, then re-run the FPS/trace capture:
mcporter call chrome-devtools.emulate --args '{"cpuThrottlingRate":4}' --output text # 4x CPU slowdown
mcporter call chrome-devtools.resize_page --args '{"width":390,"height":844}' --output text # phone viewport for resp-
mcporter call chrome-devtools.emulate --args '{"colorScheme":"dark"}' --output text # check the dark-mode passFolding captured evidence into the review
A runtime finding makes the Before column a measurement, not a guess — that is the whole point of capturing. Cite the number so the author can reproduce it:
| Severity | Before | After | Why |
|---|---|---|---|
| High | route change measured 412ms, 11 dropped frames, CLS 0.18 (performance_analyze_insight) | render a skeleton during pending; wrap the swap in startViewTransition | A blank 412ms flash with a layout jump reads as broken; interact-bridge-route-transitions |
| Medium | getAnimations() shows the toast easing resolved to linear despite the CSS ease-out | scope the transition so the keyframe isn't overridden | Linear motion feels mechanical; motion-ease-out-custom |
Guardrails
- Read-only. Capture is observation.
click/fill/navigate_pageare for walking
the app to reach the state under review — never to mutate real data. Don't run capture against production with a logged-in account that can write.
- Reattach, don't spawn. Prefer the
chrome-devtools(existing-profile) server; reach
for chrome-isolated only for a deliberately clean session.
- Don't commit artifacts. Trace JSON, screenshots, and snapshots are evidence for the
review, not repo files.
Sections
This file defines the categories and their order. The prefix in parentheses is the filename prefix that groups rules. Categories are ordered by importance × frequency — the dimensions that most often make UI look generic, unfinished, or AI-generated go first. This is a taste/correctness skill, not a performance one, so categories carry no fixed impact tier: assign each finding a severity at review time based on its impact in that specific UI.
---
1. Visual Hierarchy (hier)
Description: Whether the screen tells the eye where to look first. The single biggest reason an interface reads as "flat" or "wireframe-like" is that everything carries equal weight. Covers focal point, de-emphasis technique, primary-action dominance, and separating with space instead of borders.
2. Spacing & Layout (space)
Description: How elements are sized and grouped in space. Arbitrary, cramped, or uniform spacing destroys rhythm and hides the relationships between elements. Covers spacing scales, generous whitespace, proximity grouping, and constraining content width.
3. Typography (type)
Description: How text is sized, set, and aligned for reading. Ad-hoc font sizes, unbounded line length, and uniform line-height are the typographic tells of unconsidered UI. Covers type scales, measure, line-height, alignment, and readable body text.
4. Color & Contrast (color)
Description: Color choices that affect legibility, mood, and accessibility. Pure black, low-contrast grey, one-off hex values, and over-saturation all signal a missing system. Covers near-black text, WCAG contrast, HSL shade ramps, restrained accents, and never relying on color alone.
5. Component States & Feedback (state)
Description: Whether interactive elements respond and whether every state is designed. Default UI ships only the happy path — no press feedback, no focus ring, no empty/loading/error states. Covers active feedback, accessible focus, the full state matrix, and empty states.
6. Motion & Animation (motion)
Description: Whether motion has a purpose and is executed with the right easing, timing, and properties. The top review flags for animation; defer to the emilkowal-animations skill for the full rule set. Covers purpose/frequency, ease-out custom curves, sub-300ms timing, enter origin/scale, and animating only transform/opacity.
7. Responsiveness & Touch (resp)
Description: Whether the layout adapts and whether it works under a finger. Fixed pixel widths, sub-44px targets, and hover-only affordances break on real devices. Covers fluid mobile-first layout, touch target size, and gating hover interactions.
8. Accessibility & Semantics (access)
Description: Whether the markup conveys meaning and the experience is usable assistively. The structural accessibility wrong-defaults not already caught by color, state, or responsiveness. Covers semantic elements, accessible names for icon controls, and honoring reduced-motion preferences.
9. Flow & Navigation (flow)
Description: Whether the experience holds together across pages, not just within one screen. Single-screen review can't see that the app shell drifts route to route, that navigation drops the user's scroll and filters, that a deep link or refresh renders a broken page, or that users can't tell where they are. Reviewed by walking the multi-page flow (see _runtime-capture.md). Covers app-shell consistency, view-state persistence, entry-point integrity, and wayfinding.
10. Interaction Continuity (interact)
Description: Whether the experience stays continuous over time and across transitions as the user interacts — the dimension a screenshot cannot show. Distinct from state (how one control looks in each state): this is whether the bridge between states and views holds — route changes that flash blank, async actions that leave a dead interval, and navigations that strand keyboard/AT focus. Best reviewed against a captured trace or recording (see _runtime-capture.md). Covers bridging route transitions, spanning async feedback, and moving focus on navigation.
Give icon-only controls an accessible name
An icon-only button — a bare close "×" or a trash icon — exposes no text, so a screen reader announces only "button" and the user cannot tell what it does. Add an accessible name with aria-label, and hide the decorative icon from the accessibility tree with aria-hidden.
<button type="button" aria-label="Close dialog" onClick={onClose}>
<XIcon aria-hidden />
</button>Reference: MDN — ARIA button role
Honor the reduced-motion preference
Large transforms, parallax, and auto-playing motion can trigger nausea or vestibular discomfort for users who have asked their OS to reduce motion. Honour prefers-reduced-motion by cutting movement while keeping opacity and colour fades, which aid comprehension, rather than removing all feedback.
.modal { transition: opacity 200ms, transform 200ms; }
@media (prefers-reduced-motion: reduce) {
.modal { transition: opacity 200ms; } /* keep the fade, drop the movement */
}Reference: MDN — prefers-reduced-motion
Use semantic elements for interactive controls
A <div> with an onClick looks clickable but is invisible to assistive tech and unreachable by keyboard — it has no role, no focusability, and no Enter/Space handling. Use the real element (<button>, <a>, <nav>) and inherit those behaviours instead of re-implementing them.
Incorrect (a div pretending to be a button):
<div className="button" onClick={deleteAccount}>Delete account</div>Correct (a real button — focusable, keyboard-operable, announced):
<button type="button" className="button" onClick={deleteAccount}>
Delete account
</button>Reference: MDN — The Button element
Use a near-black instead of pure black
Pure #000 text on a white background is harsher than real-world ink and makes an interface feel stark and high-strain. Use a very dark, slightly desaturated colour so text reads softer while keeping strong contrast.
Incorrect (pure black on pure white):
body { color: #000000; background: #ffffff; }Correct (near-black, slightly cool):
body { color: hsl(222 47% 11%); background: #ffffff; } /* slate-900 */Reference: 7 Practical Tips for Cheating at Design
Define colour as HSL shade ramps
Sprinkling one-off hex values through the CSS makes it hard to keep greys and accents consistent or to derive hover and disabled variants. Define each hue as a ramp of shades in HSL, where changing only the lightness produces predictable, related steps.
:root {
--brand-100: hsl(221 83% 93%); /* tint for backgrounds */
--brand-500: hsl(221 83% 53%); /* base action colour */
--brand-600: hsl(221 83% 45%); /* hover: same hue, lower lightness */
}Reference: Refactoring UI — Defining a colour palette
Limit the palette to one accent plus neutrals
Giving each section its own bright colour produces a chaotic, toy-like interface where no hue carries meaning. Build on a neutral grey scale plus a single accent for primary actions, and add further colours only when they signal something (success, warning, danger).
:root {
--neutral-200: hsl(220 13% 91%);
--accent-500: hsl(221 83% 53%); /* the one brand action colour */
--danger-600: hsl(0 72% 45%); /* introduced only for destructive state */
}Reference: Refactoring UI — Working with colour
Meet WCAG contrast for body text
Light-grey text on white reads as refined in a mockup but commonly falls below the 4.5:1 contrast minimum, leaving it unreadable for many users and in bright light. Verify body text against at least 4.5:1 (3:1 for large text) before settling on a grey.
Incorrect (gray-400 on white — about 2.5:1, fails):
.helper-text { color: #9ca3af; background: #ffffff; }Correct (gray-500 on white — about 4.8:1, passes):
.helper-text { color: #6b7280; background: #ffffff; }Reference: WCAG 2.2 — Contrast (Minimum) 1.4.3
Pair colour with a second cue for state
Communicating status with colour alone — a red border for an error, green for success — is invisible to colour-blind users and to anyone who misses the hue. Pair colour with text or an icon so the meaning survives without it.
Incorrect (colour is the only error signal):
<input className="border border-red-500" />Correct (an icon and message carry the meaning too):
<input aria-invalid className="border border-red-500" />
<p className="text-red-700 flex items-center gap-1">
<AlertCircleIcon aria-hidden /> Enter a valid email address
</p>Reference: WCAG 2.2 — Use of Color 1.4.1
Keep the app shell consistent across pages
Pages generated one at a time drift apart — the header height, nav, container width, and page padding end up slightly different on each route, so the product feels stitched together from separate templates rather than one app. Let a single shared layout own the chrome (nav, header, max-width, vertical rhythm) and have each route render only its own content.
// app/(dashboard)/layout.tsx — every route below inherits one identical shell
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="mx-auto max-w-5xl px-6">
<SiteHeader />
<main className="py-10">{children}</main>
</div>
);
}Reference: NN/g — 10 Usability Heuristics (Consistency and standards)
Make every page work as a first entry point
A screen that reads data handed to it by the previous screen breaks the moment someone deep-links, refreshes, or opens a shared URL — the in-memory state isn't there, so the page renders empty or throws. Derive everything a page needs from the URL and load it on mount, so the route is self-sufficient no matter how the user arrived.
Incorrect (depends on state passed during navigation):
function OrderPage() {
const { state } = useLocation(); // undefined on refresh or a shared link
return <OrderDetail order={state.order} />;
}Correct (reads the id from the URL and loads its own data):
function OrderPage() {
const { orderId } = useParams();
const { data: order } = useOrder(orderId); // works on refresh, deep link, share
return order ? <OrderDetail order={order} /> : <OrderDetailSkeleton />;
}Reference: MDN — History API
Preserve scroll and view state across navigation
Client-side navigation often resets scroll to the top and drops the user's filters, sort, and search, so pressing Back lands them in a different place than they left and forces them to re-apply everything. Keep view state in the URL (search params) so it survives Back, refresh, and sharing, and let the browser restore scroll — or restore it yourself for virtualized lists where history.scrollRestoration can't.
// Filters live in the URL: Back, refresh, and a shared link all restore the same view
const [params, setParams] = useSearchParams();
const status = params.get('status') ?? 'all';
function setStatus(next: string) {
setParams(prev => { prev.set('status', next); return prev; }, { replace: true });
}Reference: MDN — History.scrollRestoration
Show where the user is and the way back
When the nav doesn't mark the current section and deep pages carry no breadcrumb, users lose track of where they are and how to climb back up a hierarchy. Mark the active route with aria-current (which both styles it and announces it), give each route a descriptive document title, and add breadcrumbs once the hierarchy runs past two levels.
// aria-current marks the active item for sighted users and assistive tech at once
<a
href="/settings/billing"
aria-current={pathname === '/settings/billing' ? 'page' : undefined}
className={pathname === '/settings/billing' ? 'font-semibold text-slate-900' : 'text-slate-500'}
>
Billing
</a>Reference: NN/g — Breadcrumbs in web design
Use colour and weight to set emphasis, not size alone
To make text secondary, the default move is to shrink it — which quickly drives it below a readable size. Reach for font weight and text colour first; reserve size changes for genuine levels of the hierarchy so secondary text stays legible.
Incorrect (de-emphasizes by shrinking until it is hard to read):
.invoice-meta { font-size: 11px; } /* secondary info, now too small to read */Correct (de-emphasizes with colour and weight, keeps a readable size):
.invoice-meta {
font-size: 14px;
font-weight: 400;
color: hsl(215 16% 47%); /* slate-500 — quieter but still legible */
}Reference: 7 Practical Tips for Cheating at Design
Establish one clear focal point per screen
By default a generated layout gives every section the same size, weight, and colour, so the eye has no entry point and the screen reads as flat. Pick the one thing that matters most on this view — the key metric, the page title, the primary action — and make it visibly dominant while everything else recedes.
// The balance dominates; label and timestamp recede in size, weight, and colour
<section className="account-summary">
<p className="text-sm font-medium text-slate-500">Available balance</p>
<p className="text-5xl font-bold tracking-tight text-slate-900">£12,480.50</p>
<p className="text-sm text-slate-500">Updated 2 minutes ago</p>
</section>Reference: Refactoring UI — Hierarchy
Limit each view to one primary action
When every button uses the same solid, high-contrast style they compete, and none reads as the main action. Give one button the primary (filled) treatment and demote the rest to secondary (outline) or tertiary (ghost) so the intended next step is obvious at a glance.
// One filled primary; the secondary action is deliberately quieter
<div className="form-actions">
<button className="bg-indigo-600 text-white font-medium px-4 py-2 rounded-md">
Save changes
</button>
<button className="text-slate-600 px-4 py-2 rounded-md hover:bg-slate-100">
Cancel
</button>
</div>Reference: Refactoring UI — Hierarchy
Replace borders with spacing and background
Reaching for a border to separate every element produces a boxy, busy interface where the lines themselves add visual noise. Separate regions with whitespace or a subtle background-shade difference first, and use a border only when two elements must sit flush.
Incorrect (a box around every row competes for attention):
.settings-row { border: 1px solid #e5e7eb; padding: 16px; }Correct (whitespace plus a single hairline divider does the separating):
.settings-row { padding: 20px 16px; }
.settings-row + .settings-row { border-top: 1px solid hsl(220 13% 91%); }Reference: 7 Practical Tips for Cheating at Design
Make values louder than their labels
Detail views and forms often render the label ("Email", "Status") with the same or greater emphasis than the actual content, so the eye lands on boilerplate instead of information. Quiet the label and let the value carry the weight.
// The label recedes; the value is the emphasised element
<div className="detail-row">
<dt className="text-xs uppercase tracking-wide text-slate-400">Status</dt>
<dd className="text-base font-semibold text-slate-900">Active</dd>
</div>Reference: Refactoring UI — Hierarchy
Bridge route changes so the screen never flashes blank
A route change that unmounts the old view and shows white space until data resolves reads as a broken cut, and the layout jumps a second time when the content finally lands. Hold the previous view or a skeleton during the pending phase, and for navigations that share a layout use the View Transitions API so the change cross-fades instead of snapping — gated behind prefers-reduced-motion so it degrades to an instant swap.
// Same-document view transition: old and new views cross-fade instead of cutting to blank
function navigate(url: string) {
if (!document.startViewTransition) return router.push(url);
document.startViewTransition(() => router.push(url));
}Reference: MDN — View Transition API
Fill the gap while an interaction is in flight
This is about time, not styling: a control that triggers async work but sits idle for hundreds of milliseconds reads as broken, so the user clicks again and double-submits. Acknowledge within ~100ms — set a pending state on the control and disable it, or apply an optimistic update and reconcile when the request settles — so the interaction feels continuous rather than fired-into-the-void. (For how each resting/disabled state should look, see state-design-all-states.)
const [pending, setPending] = useState(false);
async function onSubmit() {
setPending(true); // instant acknowledgement, before the await
try { await saveProfile(form); }
finally { setPending(false); }
}
<button onClick={onSubmit} disabled={pending}>
{pending ? 'Saving…' : 'Save'}
</button>Move focus to new content after client-side navigation
After a client-side route change the DOM updates but focus stays on the link the user clicked and nothing is announced, so keyboard and screen-reader users have no idea the view changed and must tab from the top to discover it. On each route change, move focus to the new page's main heading (made focusable with tabIndex={-1}): it's a tested pattern that drops keyboard users straight onto the new content and gives screen readers a concise announcement of where they've landed. (A polite aria-live route announcer is a common complement.)
// On every route change, move focus to the page heading so AT users land on new content
const headingRef = useRef<HTMLHeadingElement>(null);
useEffect(() => { headingRef.current?.focus(); }, [pathname]);
<h1 ref={headingRef} tabIndex={-1} className="text-3xl font-bold">
{pageTitle}
</h1>Reference: Gatsby — User testing of accessible client-side routing
Use ease-out with a custom curve for UI transitions
ease-in (and transition: all on the default curve) starts slowly, so the interface feels sluggish at the exact moment the user is watching most closely; the built-in curves are also too weak to feel intentional. Use ease-out with a stronger custom cubic-bezier, and name the properties you animate.
Incorrect (ease-in on an entering element, and animates everything):
.dropdown { transition: all 200ms ease-in; }Correct (ease-out custom curve on explicit properties):
.dropdown {
transition: opacity 180ms cubic-bezier(0.23, 1, 0.32, 1),
transform 180ms cubic-bezier(0.23, 1, 0.32, 1);
}Reference: Emil Kowalski — Great animations
Enter from a near scale and the trigger's origin
Animating a popover in from scale(0) makes it appear out of nowhere, and the default transform-origin: center makes it grow from the middle of itself instead of from the control that opened it. Start around scale(0.95) with opacity, and anchor the origin to the trigger.
Incorrect (appears from nothing, grows from its own centre):
.popover[data-state="open"] { animation: pop 150ms; }
@keyframes pop { from { transform: scale(0); } }Correct (scales up subtly from the trigger origin):
.popover { transform-origin: var(--radix-popover-content-transform-origin); }
.popover[data-state="open"] { animation: pop 150ms cubic-bezier(0.23, 1, 0.32, 1); }
@keyframes pop { from { opacity: 0; transform: scale(0.95); } }When NOT to use this pattern: modals are not anchored to a trigger — keep transform-origin: center for them.
Reference: Emil Kowalski — Great animations
Animate only with a purpose
Adding transitions to everything — especially high-frequency or keyboard-triggered actions like toggling a sidebar — makes the interface feel slow, because the user waits through the same motion hundreds of times a day. Animate to aid understanding (entrance, state change, spatial continuity), and skip it for frequent, repeated actions.
/* Occasional: a modal earns an entrance animation */
.modal { transition: opacity 200ms, transform 200ms; }
/* High-frequency: a sidebar toggled on every navigation responds instantly */
.sidebar { /* no transition — instant on each repeated use */ }Reference: Emil Kowalski — You don't need animations
Animate only transform and opacity
Animating layout properties like height, width, top, or margin forces the browser to recalculate layout and repaint on every frame, which drops frames on lower-end devices. Animate transform and opacity instead — they run on the compositor and stay smooth.
Incorrect (animates a layout property — janky):
.drawer { transition: height 300ms ease-out; }Correct (animates transform — composited):
.drawer { transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); }
.drawer[data-state="closed"] { transform: translateY(100%); }Reference: Emil Kowalski — CSS transforms
Keep UI transitions under 300ms
Durations of 400ms and up feel laggy for everyday UI because the motion outlasts the user's expectation of an instant response. Keep interface transitions in the 120–260ms range, and make the exit faster than the entrance since the user has already moved on.
.toast {
transition: transform 220ms cubic-bezier(0.23, 1, 0.32, 1);
}
.toast[data-state="closed"] {
transition-duration: 150ms; /* leave faster than it arrived */
}Reference: Emil Kowalski — Good vs great animations
Build mobile-first with fluid widths
Hard-coding pixel widths (width: 1200px) produces layouts that overflow small screens and force horizontal scrolling. Start from the small-screen layout with fluid units, then add columns at larger breakpoints with min-width media queries.
Incorrect (a fixed width overflows phones):
.dashboard-grid { width: 1200px; display: grid; grid-template-columns: repeat(3, 1fr); }Correct (fluid, mobile-first, enhancing upward):
.dashboard-grid { width: 100%; display: grid; gap: 16px; }
@media (min-width: 768px) {
.dashboard-grid { grid-template-columns: repeat(3, 1fr); }
}Reference: web.dev — Responsive web design basics
Gate hover-only affordances behind a pointer query
Putting essential controls or content behind :hover hides them on touch devices, where there is no hover and a tap can leave a sticky hover state behind. Reveal hover affordances only where a fine pointer exists, and keep the content reachable without hover.
/* Row actions are always present; hover only enhances on a mouse */
.row__actions { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
.row__actions { opacity: 0; transition: opacity 150ms; }
.row:hover .row__actions { opacity: 1; }
}Reference: MDN — hover media feature
Size touch targets to at least 44px
Icon buttons sized to the icon itself (often 20–24px) are easy to miss with a finger and frustrating on phones. Give interactive controls a hit area of at least 44×44px by padding the target out, without necessarily enlarging the visible icon.
.icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px; /* finger-friendly hit area around a 20px icon */
}Reference: Apple HIG — Layout
Cap and centre the page container width
Block elements stretch to fill their container by default, so on wide monitors a layout spans edge to edge and loses any sense of composition. Cap the container width and centre it; a full-bleed section should be a deliberate choice, not the default for everything.
.page-shell {
max-width: 1120px;
margin-inline: auto;
padding-inline: 24px;
}Reference: Refactoring UI — Layout and spacing
Vary spacing to show what is grouped
Using one uniform gap between every element makes a label, its input, and the next field all look equally related, so a form reads as an undifferentiated list. Tighten space within a group and widen it between groups so the structure is visible without drawing a single border.
/* Tight inside a field group, loose between groups */
.field { display: grid; gap: 6px; } /* label ↔ input: closely related */
.field + .field { margin-top: 24px; } /* field ↔ field: clearly separated */Reference: Refactoring UI — Grouping and proximity
Give layouts more whitespace than feels necessary
The default instinct is to fit more in by tightening space, which makes interfaces feel cramped and cheap. Start with generous padding and gaps, then remove space only where the layout feels too sparse — emptier almost always reads as more considered.
/* Generous breathing room around and within the card */
.feature-card {
padding: 32px;
display: grid;
gap: 16px;
}Reference: Refactoring UI — Whitespace
Size spacing from a consistent scale
Choosing margins and padding by eye yields values like 13px, 7px, and 22px that never quite line up, and the inconsistency reads as careless. Draw spacing from a fixed scale built on a base unit of 4px (4, 8, 12, 16, 24, 32, 48…) so the rhythm is consistent and the decisions are faster to make.
Incorrect (arbitrary one-off values):
.pricing-card { padding: 13px 19px; margin-bottom: 22px; gap: 7px; }Correct (values snapped to a 4px-based scale):
.pricing-card { padding: 16px 24px; margin-bottom: 24px; gap: 8px; }Reference: Refactoring UI — Spacing and sizing systems
Design every interactive state, not just the default
Generated components usually style only the resting state, and sometimes hover, so disabled, loading, and error states look broken or unstyled when they appear. Decide up front how each interactive element looks when hovered, focused, pressed, disabled, and loading.
// A button whose disabled and loading states are deliberately styled
<button
disabled={isSubmitting}
className="bg-indigo-600 text-white px-4 py-2 rounded-md
hover:bg-indigo-700
disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Saving…' : 'Save changes'}
</button>Reference: Refactoring UI — Designing for states
Design the empty state with guidance
When a list or dashboard has no data yet, default UI shows a blank area that looks like a bug and gives the user no next step. Design the zero-data case explicitly, with a short explanation and the action that fills it.
// The empty state explains the situation and offers the next action
<div className="empty-state">
<InboxIcon aria-hidden className="text-slate-300" />
<h3 className="font-semibold text-slate-900">No invoices yet</h3>
<p className="text-slate-500">Create your first invoice to get started.</p>
<button className="mt-4 bg-indigo-600 text-white px-4 py-2 rounded-md">
New invoice
</button>
</div>Reference: Refactoring UI — Designing empty states
Keep an accessible focus indicator
Removing the outline with outline: none to tidy up the look leaves keyboard users with no idea where focus is. Replace it with a clear :focus-visible ring, which shows for keyboard navigation but not on mouse clicks, so the page stays clean and operable.
Incorrect (kills the focus indicator entirely):
.button:focus { outline: none; }Correct (a visible ring for keyboard focus only):
.button:focus-visible {
outline: 2px solid hsl(221 83% 53%);
outline-offset: 2px;
}Reference: MDN — :focus-visible
Give pressable elements active feedback
A button that only changes on hover feels inert on click and on touch, where hover barely exists, leaving the user unsure the press registered. Add a small :active transform so the element visibly responds the instant it is pressed.
.button {
transition: transform 150ms cubic-bezier(0.23, 1, 0.32, 1);
}
.button:active {
transform: scale(0.97); /* subtle, instant confirmation of the press */
}Reference: Emil Kowalski — 7 practical animation tips
Align multi-line text to the left
Centring is the default reach for "tidy", but a centred paragraph gives every line a different starting edge, so the eye has to hunt for where the next line begins. Left-align anything longer than two lines; reserve centring for short, isolated text such as a single heading.
Incorrect (centred paragraph with a ragged left edge):
.empty-state__description { text-align: center; max-width: 60ch; }Correct (left-aligned body; centre only the short heading):
.empty-state__title { text-align: center; }
.empty-state__description { text-align: left; max-width: 60ch; }Reference: Butterick — Centered text
Limit body line length for readability
A paragraph that stretches the full width of a wide container forces the eye to track long distances and lose its place returning to the next line. Cap the measure at roughly 45–75 characters (Butterick allows up to ~90) so reading stays comfortable. Note ch is the width of the 0 glyph, so in a proportional font the rendered line runs a little longer than the ch count.
.prose p {
max-width: 66ch; /* a comfortable 65–75 character measure */
}Reference: Butterick — Line length
Set line-height relative to font size
Applying one line-height (commonly 1.5) to everything leaves large headings too loose and small print too tight, because the optimal leading shrinks as type grows. Use a tighter ratio for display sizes and a looser one for body copy.
h1 { font-size: 36px; line-height: 1.1; } /* tight leading for display */
p { font-size: 16px; line-height: 1.6; } /* roomy leading for reading */Reference: Butterick — Line spacing
Choose font sizes from a small type scale
Picking sizes ad hoc produces many near-identical values (15px, 16px, 17px, 19px) that create no clear hierarchy and look accidental. Restrict yourself to a handful of distinct steps from a modular scale so headings, body, and captions read as separate levels.
Incorrect (many arbitrary, barely-different sizes):
h1 { font-size: 29px; } h2 { font-size: 23px; } p { font-size: 15px; } small { font-size: 13px; }Correct (a few distinct steps from one scale):
h1 { font-size: 30px; } h2 { font-size: 20px; } p { font-size: 16px; } small { font-size: 14px; }Reference: Refactoring UI — Establishing a type scale
Keep body text large and solid enough to read
Generated UI often sets body copy at 13–14px in a thin weight and a pale grey, which looks elegant in a static mockup but is a strain to actually read. Keep body text near 16px with a normal weight and enough contrast to be effortless.
.card__body {
font-size: 16px;
font-weight: 400;
color: hsl(215 25% 27%); /* dark enough to read comfortably on white */
}Reference: Refactoring UI — Typography
Related skills
FAQ
What does design-review do?
design-review is a Claude Code skill for ai & agent building. It helps developers move faster with AI-assisted coding.
When should I use design-review?
When you need to helps with ai & agent building tasks during ai-assisted development, or when design-review is a claude code skill for ai & agent building. it helps developers move faster with ai-assisted coding.
What are the main capabilities?
design-review; AI & Agent Building; AI-coding skill.