Use when refining interface details (text wrapping, spacing, animations, shadows, alignment) to compound small visual improvements into polished, responsive experiences following Jakub Krehel's principles.
Scanned 8/31/2026
Install to Claude Code
npx -y skills add fernando-bertholdo/4-successful-AI-life --skill visual-polish --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Visual Polish?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/fernando-bertholdo-visual-polish)More formats (shields.io, HTML) on the badges page.
---
name: visual-polish
description: Use when refining interface details (text wrapping, spacing, animations, shadows, alignment) to compound small visual improvements into polished, responsive experiences following Jakub Krehel's principles.
---
# UI Visual Polish
## Overview
Great interfaces rarely come from a single thing—it's usually a collection of small, thoughtful details that compound into a cohesive, polished experience. This skill documents techniques for visual refinement: text wrapping strategies, balanced border radius hierarchies, contextual animations, optical alignment, and shadow composition. Each detail is intentional and measurable.
## When to Use / When NOT to Use
### Use When:
- Refining typography presentation (headings, body text, dynamic numbers)
- Designing nested UI components with multiple layers
- Adding entrance/exit animations to modals, tooltips, or lists
- Replacing solid borders with depth-aware shadows
- Aligning buttons with icons or misaligned visual elements
- Optimizing animations for user interruption (mid-toggle, mid-drag)
- Adding micro-interactions (copy-to-clipboard, status toggles)
- Ensuring consistent visual hierarchy across the interface
### Do NOT Use When:
- Building motion-heavy animations (use dedicated motion library docs)
- Solving structural layout issues (use CSS Grid/Flexbox guides)
- Implementing accessibility requirements (separate A11y guide)
- Addressing performance bottlenecks (use optimization guide)
- Creating brand identity from scratch (use design system docs)
---
## Core Techniques
### Surfaces & Layout
#### 1. Text Wrapping Strategy
Balance text distribution and reading flow with CSS `text-wrap`:
- **`text-wrap: balance`** — For headings
- Distributes text evenly across lines
- Prevents orphaned words at line ends
- Use once per heading (computed once at render)
- Best for titles, hero text, short high-impact copy
- **`text-wrap: pretty`** — For body text
- Similar to `balance` but slower algorithm
- Better for longer paragraphs
- Reduces hyphenation and ugly breaks
- May slightly reflow on resizes
**Implementation:**
```css
h1, h2, h3 {
text-wrap: balance;
}
p {
text-wrap: pretty;
}
```
#### 2. Concentric Border Radius
Create visual hierarchy in nested elements by proportional radius scaling.
**Formula:** `outer_radius = inner_radius + padding`
**Example:**
- Inner element: `border-radius: 12px`
- Padding: `8px`
- Outer container: `border-radius: 20px` (12 + 8)
This creates balanced visual nesting and guides the eye through component hierarchy without explicit borders.
```css
.card {
border-radius: 20px;
padding: 8px;
}
.card-inner {
border-radius: 12px;
padding: 8px;
}
.card-content {
border-radius: 4px;
}
```
#### 3. Shadows Over Borders
Replace flat `border` declarations with layered shadows for depth and background flexibility.
**Three-Layer Shadow Composition:**
```css
box-shadow:
0px 0px 0px 1px rgba(0, 0, 0, 0.06), /* Outline */
0px 1px 2px -1px rgba(0, 0, 0, 0.06), /* Soft inner shadow */
0px 2px 4px 0px rgba(0, 0, 0, 0.04); /* Ambient shadow */
/* Dark mode: use white with opacity */
box-shadow:
0px 0px 0px 1px rgba(255, 255, 255, 0.1),
0px 1px 2px -1px rgba(255, 255, 255, 0.06),
0px 2px 4px 0px rgba(255, 255, 255, 0.04);
/* Hover state: increase opacity */
transition: box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);
```
**Why:** Shadows adapt to any background; solid borders limit color palette and feel flat.
#### 4. Image Outlines
Apply subtle outlines to images for visual containment and depth.
```css
img {
outline: 1px solid rgba(0, 0, 0, 0.1);
outline-offset: -1px;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
img {
outline-color: rgba(255, 255, 255, 0.1);
}
}
```
Creates consistent visual framing without relying on parent backgrounds.
---
### Typography
#### 5. Font Smoothing (macOS)
macOS applies subpixel antialiasing by default, making text appear heavier than intended on light backgrounds.
**Solution:** Apply `-webkit-font-smoothing: antialiased` (or Tailwind's `antialiased` class) at layout root.
```css
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
```
**Effect:** Produces thinner, crisper rendering of light text on macOS; no effect on other OS.
#### 6. Tabular Numbers
Prevent number shifting when values update by using fixed-width numerals.
```css
.counter, .price, .metric {
font-variant-numeric: tabular-nums;
/* or Tailwind: class="tabular-nums" */
}
```
**When to use:**
- Dashboards with live-updating metrics
- Price displays with dynamic values
- Counters and timers
- Financial tables
**Caveat:** Some fonts (e.g., Inter) alter numeral appearance with `tabular-nums`. Test in your font.
---
### Animation Details
#### 7. Contextual Icon Animations
Animate icon transitions (copy→check, eye→eye-off) with opacity, scale, and blur.
**Technique:**
- **Opacity:** 0 → 1 (fade in)
- **Scale:** 0.25 → 1 (grow from center)
- **Blur:** 4px → 0px (sharpen as it enters)
- **Duration:** 300ms–400ms
**Why motion library preferred:** CSS transitions alone lack spring easing for natural deceleration.
**Example (Framer Motion syntax):**
```jsx
<AnimatePresence mode="wait">
{isCopied ? (
<motion.div
key="check"
initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, scale: 0.75 }}
transition={{ type: "spring", stiffness: 200, damping: 10 }}
>
<CheckIcon />
</motion.div>
) : (
<motion.div
key="copy"
initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, scale: 0.75 }}
transition={{ type: "spring", stiffness: 200, damping: 10 }}
>
<CopyIcon />
</motion.div>
)}
</AnimatePresence>
```
#### 8. Interruptible vs Fixed Animations
**CSS Transitions** (for interactions):
- Interpolate toward latest state
- Support interruption (user changes intent mid-animation)
- Best for: toggles, hovers, drag interactions
- User interrupts frequently; animations must retarget
**Keyframe Animations** (for sequences):
- Run fixed, predetermined timelines
- Do not retarget mid-sequence
- Best for: page-load sequences, one-time reveals
- Non-interruptible animations feel broken
**Decision Rule:** Use transitions for user interactions; keyframes for deterministic sequences.
```css
/* ✅ Transition: interruptible */
.toggle {
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.toggle:hover {
transform: scale(1.05);
}
/* ✅ Keyframe: deterministic */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-in {
animation: slideIn 800ms cubic-bezier(0.4, 0, 0.2, 1);
}
```
#### 9. Enter Animations with Stagger
Combine opacity, blur, and vertical translation for smooth entrances.
**Single Block (headings, descriptions):**
```css
@keyframes enterBlock {
from {
opacity: 0;
filter: blur(5px);
transform: translateY(8px);
}
to {
opacity: 1;
filter: blur(0);
transform: translateY(0);
}
}
.enter-block {
animation: enterBlock 800ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
```
**Staggered (buttons, list items):**
- Sectional stagger: ~100ms delays between groups
- Word-level stagger: ~80ms delays between individual items
```jsx
{buttons.map((btn, i) => (
<motion.button
key={i}
initial={{ opacity: 0, filter: "blur(5px)", y: 8 }}
animate={{ opacity: 1, filter: "blur(0px)", y: 0 }}
transition={{
duration: 0.8,
delay: i * 0.1, // Stagger by 100ms
ease: "easeOut"
}}
>
{btn.label}
</motion.button>
))}
```
#### 10. Exit Animations (Subtle)
Exits should be less prominent than entrances—they're farewell states.
**Options:**
- **Full exit:** `calc(-100% - 4px)` (slide out past container edge + padding)
- **Subtle exit:** Fixed `-12px` offset (gentle departure)
Combine with opacity fade and maintain directional motion:
```css
@keyframes exitSubtle {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-12px);
}
}
.exit-subtle {
animation: exitSubtle 400ms ease-in forwards;
}
```
---
### Interaction Details
#### 11. Scale-on-Press Feedback
Provide tactile feedback when buttons are pressed.
**Rule:** Scale exactly `0.96` (never below 0.95).
```css
button {
transition: transform 100ms cubic-bezier(0.4, 0, 0.2, 1);
}
button:active {
transform: scale(0.96);
}
```
Values below 0.95 feel exaggerated; 0.96 is subtle and satisfying.
#### 12. Optical vs Geometric Alignment
Geometric alignment doesn't always look correct visually. Adjust spacing for optical correctness.
**Common case:** Buttons with text + icon
- Icon is lighter visually, needs less padding
- Text carries weight, needs more padding
```css
.button-with-icon {
display: flex;
align-items: center;
padding-left: 12px; /* Icon side: less padding */
padding-right: 16px; /* Text side: more padding */
}
```
**Best practice:** Fix alignment in the SVG itself to avoid container adjustments.
#### 13. Minimum Interactive Hit Area
Ensure all interactive elements are at least **40×40 pixels** (mobile accessibility standard).
```css
button, a, input[type="checkbox"] {
min-width: 40px;
min-height: 40px;
}
```
---
### Performance & Best Practices
#### 14. Skip Page-Load Animations
Avoid jarring animations on first paint.
```jsx
<AnimatePresence initial={false}>
{/* Animations only on state changes, not mount */}
</AnimatePresence>
```
#### 15. Transition Property Specificity
Never use `transition: all`—specify exact properties.
```css
/* ❌ Avoid */
.element {
transition: all 200ms ease;
}
/* ✅ Specific */
.element {
transition: background-color 200ms ease, transform 200ms ease;
}
```
Reduces unnecessary repaints and makes intent clearer.
#### 16. `will-change` Usage
Reserve `will-change` for GPU-compositable properties only:
- `transform`
- `opacity`
- `filter`
```css
.animated-element {
will-change: transform, opacity;
}
```
Do NOT use on `box-shadow`, `background-color`, or layout properties—they cannot be GPU-accelerated.
---
## Quick Reference Table
| Technique | Property/Value | Use Case | Duration |
|-----------|----------------|----------|----------|
| Text Balance | `text-wrap: balance` | Headings | N/A (layout) |
| Text Pretty | `text-wrap: pretty` | Body paragraphs | N/A (layout) |
| Border Radius | `outer = inner + padding` | Nested components | N/A (layout) |
| Shadow Depth | 3-layer composition | Borders, cards | N/A (static) |
| Font Smoothing | `-webkit-font-smoothing: antialiased` | macOS rendering | N/A (static) |
| Tabular Nums | `font-variant-numeric: tabular-nums` | Counters, prices | N/A (static) |
| Icon Animate | Opacity 0→1, Scale 0.25→1, Blur 4→0px | Toggle icons | 300–400ms |
| Enter Animation | `translateY(8px), blur(5px), opacity(0)` | Staggered reveals | 800ms + stagger |
| Exit Animation | `translateY(-12px), opacity(0)` | Dismissals | 400ms |
| Scale Press | `scale(0.96)` | Button press feedback | 100ms |
| Hit Area | Min 40×40px | All interactive | N/A (layout) |
---
## Review Checklist
Before shipping a refined UI component, validate:
- [ ] **Typography**
- [ ] Headings use `text-wrap: balance`
- [ ] Body text uses `text-wrap: pretty`
- [ ] Font smoothing applied at layout root (`-webkit-font-smoothing: antialiased`)
- [ ] Dynamic numbers use `tabular-nums` if value changes
- [ ] **Spacing & Hierarchy**
- [ ] Concentric border radius follows formula (`outer = inner + padding`)
- [ ] Shadows replace flat borders (3-layer composition)
- [ ] Image outlines applied consistently (`outline: 1px solid rgba(0, 0, 0, 0.1)`)
- [ ] Optical alignment verified (text + icon buttons checked)
- [ ] **Animation**
- [ ] Enter animations use stagger (button delays ~100ms)
- [ ] Exit animations are subtle and directional
- [ ] Icon toggles animate: opacity, scale, blur
- [ ] Animations are interruptible (CSS transitions for interactions)
- [ ] `initial={false}` on AnimatePresence (skip page-load)
- [ ] **Interaction**
- [ ] Buttons scale to exactly `0.96` on press
- [ ] All interactive elements ≥40×40px
- [ ] `transition` property is specific (not `all`)
- [ ] `will-change` used only for `transform`, `opacity`, `filter`
- [ ] **Accessibility & Performance**
- [ ] Color contrast meets WCAG AA
- [ ] Motion respects `prefers-reduced-motion`
- [ ] Animations don't block critical rendering path
- [ ] No jank on 60fps target devices
---
## Common Mistakes
### ❌ Flat Borders Instead of Shadows
```css
/* ❌ Feels flat, limits backgrounds */
border: 1px solid #ccc;
/* ✅ Depth-aware, adapts to any background */
box-shadow:
0px 0px 0px 1px rgba(0, 0, 0, 0.06),
0px 1px 2px -1px rgba(0, 0, 0, 0.06),
0px 2px 4px 0px rgba(0, 0, 0, 0.04);
```
### ❌ Animation Without Interruption
```jsx
/* ❌ Feels broken if user changes intent mid-animation */
animation: spin 2s linear infinite;
/* ✅ Retargets on toggle; smooth interruption */
transition: transform 200ms ease;
```
### ❌ Over-Scaled Press Feedback
```css
/* ❌ Feels exaggerated */
button:active { transform: scale(0.90); }
/* ✅ Subtle and satisfying */
button:active { transform: scale(0.96); }
```
### ❌ Text Clipping Due to Missing Wrapping
```css
/* ❌ Creates orphans and awkward breaks */
h1 { /* no text-wrap */ }
/* ✅ Balanced distribution */
h1 { text-wrap: balance; }
```
### ❌ Hardcoded Icon Padding
```css
/* ❌ Looks misaligned */
button { padding: 12px 12px 12px 12px; }
/* ✅ Optically correct */
button { padding-left: 10px; padding-right: 14px; }
```
### ❌ No Stagger on List Reveals
```jsx
/* ❌ All items enter at once; feels abrupt */
{items.map(item => <Item />)}
/* ✅ Staggered entrance; feels intentional */
{items.map((item, i) => (
<Item delay={i * 0.08} />
))}
```
### ❌ Numbers Shifting in Tables
```css
/* ❌ Metrics jump as values update */
.metric { font-family: "Inter"; }
/* ✅ Numbers stay aligned */
.metric { font-variant-numeric: tabular-nums; }
```
---
## References & Inspiration
- **Jakub Krehel's "Details that make interfaces feel better"** — Core philosophy
- Framer Motion docs for spring easing and AnimatePresence patterns
- WCAG accessibility guidelines for color contrast and motion preferences
- CSS spec: `text-wrap`, `box-shadow`, `font-variant-numeric`
---
**Version:** 1.0.0
**Last Updated:** 2026-03-30
**Status:** Active
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!