Visual Polish Specialist
0
Upvotes
2
Views
0
Downloads
1,254
Words
Description
Apply the final visual polish to a website by implementing transitions, hover states, skeleton loaders, and subtle micro-interactions.
When to Use
I want polished transitions and hover effects. | Add skeleton loaders instead of spinners. | Make the UI feel smoother with micro-interactions. | Standardize hover feedback across components. | Implement scroll-linked animations for depth.
Use Cases
Improve hover feedback on buttons. | Add smooth page transitions between states. | Replace spinners with skeleton loaders. | Implement scroll-linked animations and micro-interactions.
SKILL.md Content
---
name: visual-polish
description: "Apply the final visual polish to a website by implementing transitions, hover states, skeleton loaders, and subtle micro-interactions."
metadata:
tags: "visual-design, web-frontend, ui-animations, hover-effects, transitions, skeleton-loaders, micro-interactions"
source: "https://skilldb.dev/skills/web-polish-skills/visual-polish"
pack: "web-polish-skills"
category: "Visual Arts & Design"
---
# Visual Polish Specialist
## When to use this skill
Use when the user says things like:
- "I want polished transitions and hover effects."
- "Add skeleton loaders instead of spinners."
- "Make the UI feel smoother with micro-interactions."
- "Standardize hover feedback across components."
- "Implement scroll-linked animations for depth."
You are the person who takes a website from "it works" to "it feels good." You add the
details that distinguish a polished product from a prototype: smooth transitions, thoughtful
hover states, skeleton loaders instead of spinners, empty states instead of blank space,
scroll-linked animations, and the kind of micro-interactions that make users go "oh, nice."
## The Polish Hierarchy
Work top-down. Each layer builds on the previous:
### Layer 1: Transitions (Do First)
Every state change should animate. No instant visual jumps.
```css
/* Global transition defaults — apply to all interactive elements */
button, a, input, select, textarea {
transition-property: color, background-color, border-color, box-shadow, opacity, transform;
transition-duration: 150ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Slower for larger elements */
.card, .panel, .modal {
transition-duration: 200ms;
}
/* Faster for tiny indicators */
.badge, .dot, .icon {
transition-duration: 100ms;
}
```
**Common issues to fix:**
- Buttons that change color without transition
- Dropdowns that appear/disappear instantly
- Hover effects with no ease
- Active states that snap instead of animate
- Tab indicators that jump instead of slide
### Layer 2: Hover States (Every Interactive Element)
If it's clickable, it must respond to hover. No exceptions.
```tsx
/* Buttons — color shift + subtle lift */
.btn:hover { background-color: var(--color-primary-700); }
/* Cards — lift + shadow increase */
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
/* Table rows — subtle highlight */
tr:hover { background-color: var(--color-gray-50); }
/* Links — underline or color shift */
a:hover { color: var(--color-primary-700); }
/* Icon buttons — background appears */
.icon-btn:hover { background-color: var(--color-gray-100); }
/* List items — background highlight */
.list-item:hover { background-color: var(--color-gray-50); }
/* Nav items — active indicator or background */
.nav-item:hover { background-color: var(--color-gray-100); }
```
### Layer 3: Focus States (Accessibility + Polish)
Visible focus rings on every focusable element. Must look intentional, not like an afterthought.
```css
/* Consistent focus ring across ALL elements */
:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--color-bg-primary), 0 0 0 4px var(--color-primary-500);
}
/* Alternative: ring-offset approach for Tailwind */
.focus-ring {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2;
}
```
### Layer 4: Loading States
Replace every blank/frozen loading moment with appropriate feedback:
**Button loading:**
```tsx
<Button loading={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
/* Shows spinner, disables interaction, changes text */
```
**Content loading — Skeletons over spinners:**
```tsx
// Skeleton that matches the content it replaces
function CardSkeleton() {
return (
<div className="rounded-lg border p-4 space-y-3 animate-pulse">
<div className="h-4 w-2/3 bg-gray-200 rounded" />
<div className="h-3 w-full bg-gray-200 rounded" />
<div className="h-3 w-4/5 bg-gray-200 rounded" />
</div>
);
}
// Use in place of real content while loading
{isLoading ? (
<div className="grid grid-cols-3 gap-4">
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</div>
) : (
<div className="grid grid-cols-3 gap-4">
{cards.map(card => <Card key={card.id} {...card} />)}
</div>
)}
```
**Page-level loading — Progress bar:**
```tsx
// Thin progress bar at top of page (like YouTube/GitHub)
function TopProgressBar({ loading }) {
return loading ? (
<div className="fixed top-0 left-0 right-0 h-0.5 z-50 bg-gray-200">
<div className="h-full bg-primary-500 animate-progress-indeterminate" />
</div>
) : null;
}
```
### Layer 5: Empty States
Every list, table, and content area needs a designed empty state:
```tsx
function EmptyState({ icon: Icon, title, description, action }) {
return (
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
<div className="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mb-4">
<Icon className="h-6 w-6 text-gray-400" />
</div>
<h3 className="text-sm font-semibold text-gray-900 mb-1">{title}</h3>
<p className="text-sm text-gray-500 max-w-sm mb-4">{description}</p>
{action}
</div>
);
}
// Usage
<EmptyState
icon={InboxIcon}
title="No messages yet"
description="When you receive messages, they'll appear here."
action={<Button size="sm">Send first message</Button>}
/>
```
### Layer 6: Error States
Every API call, form submission, and data fetch needs visible error handling:
```tsx
// Inline error for forms
<p className="text-xs text-red-600 mt-1 flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
This field is required
</p>
// Error banner for page-level issues
<div className="rounded-lg border border-red-200 bg-red-50 p-4 flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-red-500 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-red-800">Failed to load data</p>
<p className="text-sm text-red-700 mt-1">Please try again or contact support.</p>
<Button size="sm" variant="outline" className="mt-3" onClick={retry}>
Try again
</Button>
</div>
</div>
// Toast for transient errors
toast.error('Failed to save changes. Please try again.');
```
### Layer 7: Scroll Behavior
```css
/* Smooth scroll for anchor links */
html {
scroll-behavior: smooth;
}
/* Scroll margin for sticky headers */
[id] {
scroll-margin-top: 5rem; /* Height of sticky header + gap */
}
/* Scrollbar styling (Webkit) */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-gray-300);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-gray-400);
}
```
### Layer 8: Micro-interactions (The Premium Feel)
Small details that separate good from great:
```tsx
// Copy button with feedback
function CopyButton({ text }) {
const [copied, setCopied] = useState(false);
return (
<button
onClick={() => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}}
className="p-1.5 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors"
>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
);
}
// Tooltips on icon buttons
<Tooltip content="Copy to clipboard">
<CopyButton text={apiKey} />
</Tooltip>
// Count animations
<motion.span key={count}>
{count}
</motion.span>
// Success checkmark after save
{saved && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="text-green-500"
>
<Check className="h-4 w-4" />
</motion.span>
)}
```
## The Polish Audit Checklist
### Before you ship, verify:
**Transitions:**
- [ ] No instant color changes on any interactive element
- [ ] No instant show/hide on any dropdown, tooltip, or modal
- [ ] All transitions use the same easing function
- [ ] All transitions are 100-300ms (never slower)
**Hover:**
- [ ] Every button has a hover state
- [ ] Every link has a hover state
- [ ] Every card has a hover state (if clickable)
- [ ] Every table row has a hover state
- [ ] Every nav item has a hover state
- [ ] No orphan elements that are clickable but don't respond to hover
**Focus:**
- [ ] Every focusable element has a visible focus ring
- [ ] Focus rings are the same style everywhere
- [ ] Tab order is logical
**Loading:**
- [ ] Every button that triggers an async action shows a loading state
- [ ] Every data fetch shows a skeleton or spinner
- [ ] No blank white screens during loading
**Empty:**
- [ ] Every list has an empty state
- [ ] Every search has a "no results" state
- [ ] Every table has an empty state
**Error:**
- [ ] Every form field has an error state
- [ ] Every API call has error handling UI
- [ ] Errors are dismissible or auto-dismiss
**Scroll:**
- [ ] No horizontal scrollbars on any viewport width
- [ ] Smooth scroll for anchor links
- [ ] Sticky header doesn't overlap scrolled-to content
## Anti-Patterns
- Don't add animations just because you can. Every animation must serve a purpose (guide
attention, provide feedback, show spatial relationships).
- Don't make transitions too slow. 150-200ms for most things, 300ms max for large elements.
Anything over 500ms feels broken.
- Don't use spinners where skeletons are appropriate. Skeletons show structural preview;
spinners show nothing.
- Don't polish what's broken. Fix functionality first, then polish.
- Don't add motion for users who prefer reduced motion:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```