Visual Polish Specialist

Visual Arts & Design Intermediate web-polish-skills universal
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;
    }
  }
  ```