From ui-excellence
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ui-excellence:visual-polishThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
Balance text distribution and reading flow with CSS text-wrap:
text-wrap: balance — For headings
text-wrap: pretty — For body text
balance but slower algorithmImplementation:
h1, h2, h3 {
text-wrap: balance;
}
p {
text-wrap: pretty;
}
Create visual hierarchy in nested elements by proportional radius scaling.
Formula: outer_radius = inner_radius + padding
Example:
border-radius: 12px8pxborder-radius: 20px (12 + 8)This creates balanced visual nesting and guides the eye through component hierarchy without explicit borders.
.card {
border-radius: 20px;
padding: 8px;
}
.card-inner {
border-radius: 12px;
padding: 8px;
}
.card-content {
border-radius: 4px;
}
Replace flat border declarations with layered shadows for depth and background flexibility.
Three-Layer Shadow Composition:
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.
Apply subtle outlines to images for visual containment and depth.
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.
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.
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.
Prevent number shifting when values update by using fixed-width numerals.
.counter, .price, .metric {
font-variant-numeric: tabular-nums;
/* or Tailwind: class="tabular-nums" */
}
When to use:
Caveat: Some fonts (e.g., Inter) alter numeral appearance with tabular-nums. Test in your font.
Animate icon transitions (copy→check, eye→eye-off) with opacity, scale, and blur.
Technique:
Why motion library preferred: CSS transitions alone lack spring easing for natural deceleration.
Example (Framer Motion syntax):
<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>
CSS Transitions (for interactions):
Keyframe Animations (for sequences):
Decision Rule: Use transitions for user interactions; keyframes for deterministic sequences.
/* ✅ 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);
}
Combine opacity, blur, and vertical translation for smooth entrances.
Single Block (headings, descriptions):
@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):
{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>
))}
Exits should be less prominent than entrances—they're farewell states.
Options:
calc(-100% - 4px) (slide out past container edge + padding)-12px offset (gentle departure)Combine with opacity fade and maintain directional motion:
@keyframes exitSubtle {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-12px);
}
}
.exit-subtle {
animation: exitSubtle 400ms ease-in forwards;
}
Provide tactile feedback when buttons are pressed.
Rule: Scale exactly 0.96 (never below 0.95).
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.
Geometric alignment doesn't always look correct visually. Adjust spacing for optical correctness.
Common case: Buttons with text + icon
.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.
Ensure all interactive elements are at least 40×40 pixels (mobile accessibility standard).
button, a, input[type="checkbox"] {
min-width: 40px;
min-height: 40px;
}
Avoid jarring animations on first paint.
<AnimatePresence initial={false}>
{/* Animations only on state changes, not mount */}
</AnimatePresence>
Never use transition: all—specify exact properties.
/* ❌ Avoid */
.element {
transition: all 200ms ease;
}
/* ✅ Specific */
.element {
transition: background-color 200ms ease, transform 200ms ease;
}
Reduces unnecessary repaints and makes intent clearer.
will-change UsageReserve will-change for GPU-compositable properties only:
transformopacityfilter.animated-element {
will-change: transform, opacity;
}
Do NOT use on box-shadow, background-color, or layout properties—they cannot be GPU-accelerated.
| 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) |
Before shipping a refined UI component, validate:
Typography
text-wrap: balancetext-wrap: pretty-webkit-font-smoothing: antialiased)tabular-nums if value changesSpacing & Hierarchy
outer = inner + padding)outline: 1px solid rgba(0, 0, 0, 0.1))Animation
initial={false} on AnimatePresence (skip page-load)Interaction
0.96 on presstransition property is specific (not all)will-change used only for transform, opacity, filterAccessibility & Performance
prefers-reduced-motion/* ❌ 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);
/* ❌ Feels broken if user changes intent mid-animation */
animation: spin 2s linear infinite;
/* ✅ Retargets on toggle; smooth interruption */
transition: transform 200ms ease;
/* ❌ Feels exaggerated */
button:active { transform: scale(0.90); }
/* ✅ Subtle and satisfying */
button:active { transform: scale(0.96); }
/* ❌ Creates orphans and awkward breaks */
h1 { /* no text-wrap */ }
/* ✅ Balanced distribution */
h1 { text-wrap: balance; }
/* ❌ Looks misaligned */
button { padding: 12px 12px 12px 12px; }
/* ✅ Optically correct */
button { padding-left: 10px; padding-right: 14px; }
/* ❌ All items enter at once; feels abrupt */
{items.map(item => <Item />)}
/* ✅ Staggered entrance; feels intentional */
{items.map((item, i) => (
<Item delay={i * 0.08} />
))}
/* ❌ Metrics jump as values update */
.metric { font-family: "Inter"; }
/* ✅ Numbers stay aligned */
.metric { font-variant-numeric: tabular-nums; }
text-wrap, box-shadow, font-variant-numericVersion: 1.0.0 Last Updated: 2026-03-30 Status: Active
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub fernando-bertholdo/4-successful-ai-life --plugin ui-excellence