AI Skill Library

Micro-interactions & UX Animations

Button feedback, loading states, skeleton screens, toasts, drag-and-drop UX.

uxanimationreactfrontenddesign
# Micro-interactions & UX Animations

## Button states
```css
/* Ripple effect */
.btn { position: relative; overflow: hidden; }
.btn::after {
  content: '';
  position: absolute;
  inset: -50%;
  background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 60%);
  transform: scale(0);
  transition: transform 0.4s, opacity 0.4s;
  opacity: 0;
}
.btn:active::after { transform: scale(1); opacity: 1; transition: none; }

/* Magnetic hover */
```
```ts
// Magnetic button (follows cursor)
const btn = document.querySelector('.btn-magnetic')!
btn.addEventListener('mousemove', (e: MouseEvent) => {
  const rect = btn.getBoundingClientRect()
  const cx = rect.left + rect.width / 2
  const cy = rect.top + rect.height / 2
  const dx = (e.clientX - cx) * 0.3
  const dy = (e.clientY - cy) * 0.3
  ;(btn as HTMLElement).style.transform = `translate(${dx}px, ${dy}px)`
})
btn.addEventListener('mouseleave', () => {
  ;(btn as HTMLElement).style.transform = ''
})
```

## Skeleton screens
```css
.skeleton {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}
@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.dark .skeleton {
  background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
  background-size: 200% 100%;
}
```
```tsx
// React skeleton component
function SkeletonCard() {
  return (
    <div className="rounded-lg border p-4 space-y-3">
      <div className="skeleton h-4 w-3/4" />
      <div className="skeleton h-3 w-full" />
      <div className="skeleton h-3 w-5/6" />
      <div className="skeleton h-8 w-24 mt-4" />
    </div>
  )
}
```

## Loading states (button)
```tsx
function SubmitButton({ loading, children }) {
  return (
    <button disabled={loading} className="relative">
      <span className={loading ? 'opacity-0' : 'opacity-100'}>{children}</span>
      {loading && (
        <span className="absolute inset-0 flex items-center justify-center">
          <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
            <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"
              className="opacity-25" fill="none" />
            <path className="opacity-75" fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
          </svg>
        </span>
      )}
    </button>
  )
}
```

## Toast notifications
```tsx
import { toast } from 'sonner'
// Install: npm install sonner
// In layout: <Toaster richColors position="top-right" />

tost.success('Saved!', { description: 'Your changes have been saved.' })
toast.error('Failed', { description: error.message })
toast.promise(saveData(), {
  loading: 'Saving...',
  success: 'Saved!',
  error: 'Failed to save.',
})
toast.custom(() => <div className="flex gap-2"><span>Custom toast</span></div>)
```

## Drag and drop (dnd-kit)
```bash
npm install @dnd-kit/core @dnd-kit/sortable
```
```tsx
import { DndContext, closestCenter } from '@dnd-kit/core'
import { SortableContext, useSortable, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'

function SortableItem({ id, children }) {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })
  return (
    <div
      ref={setNodeRef}
      style={{ transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1 }}
      {...attributes} {...listeners}
    >
      {children}
    </div>
  )
}

<DndContext collisionDetection={closestCenter} onDragEnd={({ active, over }) => {
  if (over && active.id !== over.id) {
    setItems(items => arrayMove(items, items.indexOf(active.id), items.indexOf(over.id)))
  }
}}>
  <SortableContext items={items} strategy={verticalListSortingStrategy}>
    {items.map(id => <SortableItem key={id} id={id}>{id}</SortableItem>)}
  </SortableContext>
</DndContext>
```

## Number counter animation
```ts
function animateCount(el: HTMLElement, from: number, to: number, duration = 1000) {
  const start = performance.now()
  const update = (now: number) => {
    const p = Math.min((now - start) / duration, 1)
    const ease = 1 - Math.pow(1 - p, 3)  // ease-out-cubic
    el.textContent = Math.round(from + (to - from) * ease).toLocaleString()
    if (p < 1) requestAnimationFrame(update)
  }
  requestAnimationFrame(update)
}
```

API: /api/skills/micro-interactions