Visual Polish
Overview
Run the systematic quality pass that turns "it works" into "this feels professional." Output is .forge/polish-checklist.md — pass/fail per item, with file:line for every failure. Reads design-system, component-library, page-composition (all optional, but each makes the pass sharper). Sits between /build and /ship in the pipeline.
When to Use
- Features are built, tests pass, and the UI is about to be shown to anyone outside the team
- A demo or investor meeting is scheduled within the week
- The product "works" but the team feels something is off and can't name it
- A redesign was promised and the team needs to confirm the redesign actually landed
- Before promoting a beta to general availability
When NOT to Use
- Features aren't built yet — polish is the last 10%, not the first 90%
- A backend-only service
- Polish was done this sprint and nothing visual changed since
Common Rationalizations
| Thought | Reality |
|---|---|
| "It works, that's enough" | Working is table stakes. Users judge quality in the first 3 seconds. Polish is the difference between "this works" and "this feels professional." |
| "We'll polish before launch" | Launch is always tomorrow. If polish isn't in the process, it never happens. |
| "Users don't notice these details" | Users can't articulate what they notice. They feel "janky" or "solid." Polish is what they're feeling. |
| "It's just a demo" | Demos are the highest-stakes UI moment. Every missing hover state says "prototype." |
| "We'll fix it after the demo" | After the demo means after the impression. The first impression is the only impression. |
Red Flags
- Browser default focus rings (
outline: auto) - Empty state that says only "No data" with no context or CTA
- Error message that says "Something went wrong" with no next step
- Layout shift on content load (CLS > 0.1)
- Missing favicon — the tab is generic
- Page title that says
localhost:3000orVite App - Horizontal scroll appearing at 320px or 1440px
Core Process
Step 1: Read available UI artifacts
Load whichever of these exist: .forge/design-system.md, .forge/component-library.md, .forge/page-composition.md, .forge/brand-identity.md, .forge/interaction-patterns.md, references/motion-system.md. Use them as the rubric — polish is "the runtime matches the system."
Step 2: Micro-interaction audit
For every interactive element:
- Every button has
hover+active+focus-visiblestates - Every toggle/switch transitions smoothly (not instant swap; per
references/motion-system.md) - Every link has a hover state (underline or color shift)
- Focus rings are visible and consistent (not browser defaults; not
outline: nonewithout replacement) - Form inputs have
focus+error+disabledstyling
Step 3: Empty state audit
For every list, table, chart, dashboard panel, sidebar group:
- An empty state exists with helpful headline + description + CTA
- No blank areas; no "No data" without context
- Empty-state voice matches
brand-identityvoice rules
Step 4: Error state audit
- Every form has inline validation with specific messages (not just a red border)
- Every network call has error handling with retry where applicable
- A 404 page exists and matches the product's visual language
- Toast notifications for async errors are styled (not
alert()) - Error messages name what happened and what to do next
Step 5: Loading audit
- Every async content area has a skeleton screen (not a spinner; per
interaction-patterns) - Skeletons match the shape of real content (same dimensions, same approximate density)
- Zero layout shift when content loads (skeleton → content occupies identical box)
- Spinners only inside buttons (
spinner-in-button+ disabled state)
Step 6: Responsive edge case audit
Test at the extremes:
- 320px — smallest common phone; no horizontal scroll, no text overflow without ellipsis
- 1440px+ — large desktop; content respects the max-width cap from
page-composition - Touch targets ≥ 44pt on mobile
- Long content (multi-word labels, 100-character strings, pasted URLs) tested in every container
Step 7: Meta audit
- Favicon exists and matches
brand-identity - Apple touch icon for iOS home-screen save
- OG meta tags:
og:title,og:description,og:image— image is 1200×630 and matches brand <title>is descriptive per route (not the same string everywhere)<meta name="theme-color">set for mobile browser chrome
Step 8: Dark mode audit (if applicable)
- Every screen rendered in dark mode
- All text/background pairings pass WCAG AA (4.5:1 for body, 3:1 for large text)
- Images and illustrations have dark-mode variants or transparent backgrounds
- No hardcoded
#FFFFFFleaking through (grep for raw hex in component files) - Inline SVGs use
currentColorwhere appropriate
Step 9: Write .forge/polish-checklist.md
For every audit, list each item as pass/fail. Every failure includes:
- The audit it belongs to
- A
file:line(or route + selector) pointer - The smallest specific fix
Prepend a forge:meta header (generated_by: visual-polish, generated_at: <ISO 8601 UTC with Z>, depends_on: [<each UI artifact that existed at run time>] — paths only, never hashes, generated_from: {<each upstream path>: <its content_hash AT generation time>}, content_hash: <sha256 first 8 of THIS file's body>).
After writing, report a summary: total items checked, total failed, the three highest-impact failures.
Verification
- Every interactive element has
default / hover / active / focus-visible / disabledstates (loading + error where relevant) - Every list/table/chart has an empty state with CTA
- Every async area has a skeleton, not a spinner on the content
- No layout shift on content load (CLS ≈ 0)
- Favicon, Apple touch icon, OG tags, and route-specific
<title>all present - No horizontal scroll at 320px or 1440px+
- Touch targets ≥ 44pt on mobile
- Dark mode parity (if shipped) — all pairings pass WCAG AA
-
.forge/polish-checklist.mdwritten withfile:linepointers for every failure - Summary reported with the three highest-impact failures called out