ui-polish · diff

git:20260825.06c64c5 to git:20260826.41a30c8

2 added, 34 removed. Audit A to A.

---
name: ui-polish
description: >-
Design-engineering details that make an interface feel polished — border radius, optical alignment, shadows and elevation, animations and micro-interactions, press feedback, icons. INVOKE PROACTIVELY when building or reviewing UI components, adding motion or hover/active states, or when the user says "make it feel better" or "something feels off". Text rendering: [[typography]]; hit areas, focus, reduced motion: [[accessibility]]; structure: [[layout]]; whole-screen audits: [[interface-review]].
---
# Details that make interfaces feel better
Great interfaces rarely come from a single thing. It's usually a collection of small details that compound into a great experience. Apply these principles when building or reviewing UI code.
When reviewing, slow the interface down: replay motion at 10% speed in the browser's Animations panel and walk every state: hover, focus, active, loading, empty. What feels off at 10% speed is what's subtly wrong at full speed.
Preserve the project's component library, tokens, and density. Match its established motion language except where a principle below prescribes an exact interaction pattern.
Typography (text wrapping, font rendering, tabular numbers, spacing) is covered by [[typography]]; use that for anything text-related. Accessibility (hit areas, focus states, keyboard support, ARIA, reduced motion) is covered by [[accessibility]]. Layout structure (grouping, spacing between sections, breakpoints, spatial RTL) is covered by [[layout]].
## Core Principles
### 1. Concentric Border Radius
Outer radius = inner radius + padding. Mismatched radii on nested elements is the most common thing that makes interfaces feel off.
→ `references/surfaces.md` — the radius arithmetic, optical-alignment adjustments, layered shadow recipes, and image-outline values behind §1–§3 and §8, read when this change nests rounded surfaces, adds elevation, or places an image.
### 2. Optical Over Geometric Alignment
When geometric centering looks off, align optically. Buttons with icons, play triangles, and asymmetric icons all need manual adjustment.
### 3. Shadows for Elevation, Borders for Structure
For buttons, cards, and containers whose border exists only to create depth, prefer layered transparent `box-shadow` values. Keep borders that communicate structure or state: dividers, layout separators, and selected or focus states.
### 4. Interruptible Animations
Use CSS transitions for interactive state changes: they can be interrupted mid-animation. Reserve keyframes for staged sequences that run once.
→ `references/animations.md` — interruptible-transition patterns, enter/exit and stagger timings, the icon cross-fade recipes, press feedback, and the motion-restraint rules behind §4–§10 and §15, read when this change adds or edits any animation or transition.
### 5. Split and Stagger Enter Animations
For an infrequent staged entrance where sequence helps communicate hierarchy, break content into semantic chunks and stagger them by ~100ms instead of animating one container. Do not stagger routine, high-frequency interactions.
### 6. Subtle Exit Animations
Use a small fixed `translateY` instead of full height. Exits should be softer than enters. Use `ease-out` for both enter and exit transitions.
### 7. Contextual Icon Animations
Animate icons with `opacity`, `scale`, and `blur` instead of toggling visibility. Use exactly these values: scale from `0.25` to `1`, opacity from `0` to `1`, blur from `4px` to `0px`. Under `prefers-reduced-motion`, [[accessibility]]'s rule wins over these values: replace the scale/blur entrance with an opacity-only crossfade — the exact values govern the full-motion variant only. If the project has `motion` or `framer-motion` in `package.json`, match that package's import path (or the established nearby imports when both exist) and use `transition: { type: "spring", duration: 0.3, bounce: 0 }`; bounce must always be `0`. If no motion library is installed, keep both icons in the DOM (one absolute-positioned) and cross-fade with CSS transitions using `cubic-bezier(0.2, 0, 0, 1)`; this gives both enter and exit animations without any dependency.
### 8. Image Outlines
Add a subtle `1px` outline with low opacity to images for consistent depth. The color must be pure black in light mode (`oklch(0 0 0 / 0.1)`) and pure white in dark mode (`oklch(1 0 0 / 0.1)`), never a near-black like slate, zinc, or any tinted neutral. A tinted outline picks up the surface color underneath it and reads as dirt on the image edge. The *pure-black/white at low alpha* is the non-negotiable part, not the literal notation: in a project with a semantic token system, express the value through a token in the project's existing notation ([[colors]]' rule) rather than pasting a raw `oklch()` string.
### 9. Scale on Press
A subtle `scale(0.96)` on click gives buttons tactile feedback. Default to `0.96`; honor an established project value down to `0.95`, and never go below `0.95` — anything smaller feels exaggerated. Add a `static` prop to disable it when motion would be distracting.
### 10. Skip Animation on Page Load
Use `initial={false}` on `AnimatePresence` to prevent enter animations on first render. Verify it doesn't break intentional entrance animations.
### 11. Never Use `transition: all`
Always specify exact properties: `transition-property: scale, opacity`. Tailwind's `transition-transform` covers `transform, translate, scale, rotate`.
→ `references/performance.md` — transition specificity and the `will-change` rules behind §11–§12, read when this change animates a property or you hit first-frame stutter.
### 12. Use `will-change` Sparingly
Only for `transform`, `opacity`, `filter`, the properties the GPU can composite. Never use `will-change: all`. Only add when you notice first-frame stutter.
### 13. Match Icon Stroke to Text Weight
An icon next to text carries the text's optical weight: `1.5px` stroke beside regular (400) text, `2px` beside semibold (600). One stroke weight per icon set; never mix libraries on one surface.
→ `references/icons.md` — stroke weights, `currentColor` state handling, outline vs. fill, sizing, and RTL flipping behind §13–§14, read when this change adds, swaps, or restyles an icon.
### 14. One SVG, Recolored per State
Icons use `currentColor` and get their states (hover, selected, disabled) from CSS color and opacity, never from separate assets. Outline variant is the default; fill variant marks the active state.
### 15. Motion Restraint
No custom animation on high-frequency interactions: the attention cost repeats on every trigger. Motion is never the only feedback channel; every animated state change also needs a static cue (color, icon, label).
## Common Mistakes
| Mistake | Fix |
| --- | --- |
| Same border radius on closely nested parent and child | Calculate `outerRadius = innerRadius + padding` |
| Icons look off-center | Adjust optically with padding or fix SVG directly |
| Border used only to fake elevation | Use layered `box-shadow` with transparency; keep structural and state borders |
| Jarring staged entrance or contextual exit | Stagger infrequent entrances and keep context-preserving exits subtle |
| Stateful icon or toggle animates its default state on page load | Add `initial={false}` to that `AnimatePresence`; preserve intentional page entrances |
| `transition: all` on elements | Specify exact properties |
| First-frame animation stutter | Add `will-change: transform` (sparingly) |
| Hairline icon beside bold text | Match the stroke width to the text weight |
| Separate icon assets per state | One `currentColor` SVG, states via CSS |
| Filled icons everywhere | Outline as default, fill only for the active state |
| Entrance animation on every hover or keystroke | Instant feedback or ≤150ms opacity/color transition |
## Review Output Format
Use this format only when the user asks for a standalone UI-polish review. When [[interface-review]] orchestrates the review, provide domain evidence and findings to that skill and let its output format, severity scale, consolidation rules, cap, and verdict take precedence.
- Present the standalone review in two parts.
-
- ### Findings
-
- Group all confirmed findings by principle. Use a markdown table with **Severity**, **Location**, **Before**, **After**, and **Why** columns. Never use separate "Before:" / "After:" lines.
-
- - **Severity**: `HIGH` makes an interaction misleading, unresponsive, or repeatedly disruptive; `MEDIUM` creates a noticeable craft or consistency problem; `LOW` is isolated polish.
- - **Location**: cite `path/to/file:line`. If the artifact has no source files, cite the exact screen and component instead.
- - **Before / After**: show the current implementation and an actionable replacement.
- - **Why**: name the violated principle and explain how it affects the interface.
-
- Consolidate a repeated systemic issue into one row and list every affected location. Omit principles with no findings.
-
- ### Example
-
- #### Concentric border radius
- | Severity | Location | Before | After | Why |
- | --- | --- | --- | --- | --- |
- | LOW | `src/Card.tsx:28` | `rounded-xl` on card + `rounded-xl` on inner button (`p-2`) | `rounded-2xl` on card (`8 + 8 = 16`), `rounded-lg` on inner button | Nested corners should be concentric |
- | LOW | `src/card.css:11` | `border-radius: 16px` on both nested surfaces | Outer `24px`, inner `16px` with `8px` padding | Equal nested radii make the inner surface look pinched |
-
- #### Scale on press
- | Severity | Location | Before | After | Why |
- | --- | --- | --- | --- | --- |
- | LOW | `src/Button.tsx:19` | `<button className="...">` | Add `active:scale-[0.96] transition-transform` | Press feedback makes the control feel responsive |
- | MEDIUM | `src/button.css:24` | `scale(0.9)` on press | Raise to `scale(0.96)` | Anything below `0.95` feels exaggerated |
-
- ### Verification and Verdict
-
- After the findings:
-
- 1. **Verification**: list the exact checks run and their observed results. Walk every relevant state, and inspect motion at 10% speed in the browser's Animations panel when animation is involved — when no browser is available to you, verify the motion values from source and name the slowed-replay inspection as still needing a human or a rendered preview. If a check was not run, state what still needs verification.
- 2. **Verdict**: `Block` if any `HIGH` finding remains, `Needs changes` if only `MEDIUM` or `LOW` findings remain, and `Approve` only when no actionable findings remain.
+ Report all confirmed findings as one markdown table ordered by severity — `| Severity | Location | Before | After | Why |`, never separate "Before:" / "After:" lines. **Location** cites `path/to/file:line` (or the exact screen and component when there are no source files); **Before / After** show the current implementation and an actionable replacement; **Why** names the violated principle and its impact. Consolidate a repeated systemic issue into one row listing every affected location. **Severity**: `HIGH` makes an interaction misleading, unresponsive, or repeatedly disruptive; `MEDIUM` creates a noticeable craft or consistency problem; `LOW` is isolated polish.
- When there are no findings, omit the tables, state "No actionable UI-polish findings", report verification, and end with `Approve`.
+ After the findings: **Verification** — list the exact checks run and their observed results (every relevant state, plus motion inspected at 10% speed in the browser's Animations panel — or verified from source with the slowed replay named as still needing a human when applicable), and name any check not run. Then **Verdict**: `Block` if any `HIGH` finding remains, `Needs changes` if only `MEDIUM` or `LOW` findings remain, `Approve` only when no actionable findings remain. When there are no findings, omit the table, state "No actionable UI-polish findings", report verification, and end with `Approve`.
## Reference files
| File | What it answers |
|---|---|
| `references/surfaces.md` | Border radius, optical alignment, shadows, image outlines (§1–§3, §8) |
| `references/animations.md` | Interruptible animations, enter/exit transitions, icon animations, scale on press, motion restraint (§4–§10, §15) |
| `references/icons.md` | Icon stroke weight, states via `currentColor`, outline vs. fill, sizing, RTL flipping (§13–§14) |
| `references/performance.md` | Transition specificity, `will-change` usage (§11–§12) |
## Provenance and maintenance
Last verified 2026-07. The volatile claims here are the ecosystem-dependent ones: the `motion` / `framer-motion` package names, import paths, and API surface in §7 and §10 (`AnimatePresence`, `initial={false}`, the spring `transition` object), and the set of properties Tailwind's `transition-transform` covers in §11. Re-verify against the installed package's own documentation and the project's Tailwind version before relying on a specific API shape; the motion principles and the exact numeric values they prescribe are stable.