Claude-skill-registry animator

Animation and micro-interaction patterns for web interfaces. Use when adding transitions, animations, hover effects, loading states, or any motion to UI components.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/animator" ~/.claude/skills/majiayu000-claude-skill-registry-animator && rm -rf "$T"
manifest: skills/data/animator/SKILL.md
source content

Motion Design

Create meaningful, performant animations that enhance user experience.

Core Principles

Purpose of Motion

  • Feedback - Confirm user actions (button press, form submit)
  • Orientation - Show where elements come from/go to
  • Focus - Direct attention to important changes
  • Delight - Add personality without slowing users down

When NOT to Animate

  • User has
    prefers-reduced-motion
    enabled
  • Animation would delay critical actions
  • Motion doesn't add meaning
  • On low-powered devices
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Timing & Easing

Duration Guidelines

TypeDurationUse Case
Micro100-150msButton states, toggles, small feedback
Standard200-300msMost UI transitions, modals, dropdowns
Complex300-500msPage transitions, large reveals
Emphasis500ms+Onboarding, celebrations (use sparingly)

Easing Functions

/* Natural motion - use for most UI */
--ease-out: cubic-bezier(0.0, 0.0, 0.2, 1);      /* Decelerate */
--ease-in: cubic-bezier(0.4, 0.0, 1, 1);          /* Accelerate */
--ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1);   /* Both */

/* Expressive motion - entrances/exits */
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);  /* Overshoot */
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);   /* Playful */

/* Quick reference */
ease-out: Elements entering (coming to rest)
ease-in: Elements exiting (accelerating away)
ease-in-out: Elements moving between states

Tailwind Defaults

<!-- Duration -->
duration-75 duration-100 duration-150 duration-200 duration-300 duration-500

<!-- Easing -->
ease-linear ease-in ease-out ease-in-out

Common Patterns

Button Interactions

.button {
  transition: transform 150ms ease-out,
              box-shadow 150ms ease-out,
              background-color 150ms ease-out;
}

.button:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.button:active {
  transform: translateY(0) scale(0.98);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
// Tailwind
<button className="transition-all duration-150 ease-out
  hover:-translate-y-0.5 hover:shadow-lg
  active:translate-y-0 active:scale-[0.98]">
  Click me
</button>

Fade & Scale Enter

/* Modal/Dialog entrance */
@keyframes fadeScaleIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.modal {
  animation: fadeScaleIn 200ms ease-out;
}

Slide Transitions

/* Slide from bottom */
@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Slide from side (for drawers) */
@keyframes slideInRight {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

Staggered List Animation

// Framer Motion
<motion.ul>
  {items.map((item, i) => (
    <motion.li
      key={item.id}
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ delay: i * 0.05 }}
    />
  ))}
</motion.ul>
/* CSS stagger with animation-delay */
.list-item {
  opacity: 0;
  animation: fadeSlideIn 300ms ease-out forwards;
}

.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 50ms; }
.list-item:nth-child(3) { animation-delay: 100ms; }
/* ... or use CSS custom properties */

.list-item {
  animation-delay: calc(var(--index) * 50ms);
}

Loading States

/* Pulse (skeleton loading) */
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.skeleton {
  animation: pulse 2s ease-in-out infinite;
}

/* Spinner */
@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  animation: spin 1s linear infinite;
}

/* Progress bar shimmer */
@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

.shimmer {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

Hover Reveals

/* Image zoom on hover */
.image-container {
  overflow: hidden;
}

.image-container img {
  transition: transform 300ms ease-out;
}

.image-container:hover img {
  transform: scale(1.05);
}

/* Underline grow */
.link {
  position: relative;
}

.link::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 250ms ease-out;
}

.link:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

Framer Motion Patterns

Basic Animation

import { motion } from 'framer-motion';

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: -20 }}
  transition={{ duration: 0.2, ease: 'easeOut' }}
>
  Content
</motion.div>

Variants for Complex Animations

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.05,
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
};

<motion.ul variants={containerVariants} initial="hidden" animate="visible">
  {items.map((item) => (
    <motion.li key={item.id} variants={itemVariants}>
      {item.name}
    </motion.li>
  ))}
</motion.ul>

Layout Animations

// Animate layout changes automatically
<motion.div layout>
  {isExpanded ? <ExpandedContent /> : <CollapsedContent />}
</motion.div>

// Shared layout animation (element morphing)
<motion.div layoutId="shared-element">
  {/* This element animates between positions */}
</motion.div>

Gestures

<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
  Press me
</motion.button>

AnimatePresence for Exit Animations

import { AnimatePresence, motion } from 'framer-motion';

<AnimatePresence mode="wait">
  {isVisible && (
    <motion.div
      key="modal"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      Modal content
    </motion.div>
  )}
</AnimatePresence>

GSAP Patterns

Basic Animation

import gsap from 'gsap';

// Simple tween
gsap.to('.element', {
  x: 100,
  opacity: 1,
  duration: 0.3,
  ease: 'power2.out'
});

// From animation
gsap.from('.element', {
  y: 20,
  opacity: 0,
  duration: 0.3,
  ease: 'power2.out'
});

Timeline for Sequences

const tl = gsap.timeline();

tl.from('.header', { y: -50, opacity: 0 })
  .from('.content', { y: 20, opacity: 0 }, '-=0.2')
  .from('.footer', { y: 20, opacity: 0 }, '-=0.2');

// Control the timeline
tl.play();
tl.pause();
tl.reverse();

Stagger Animations

gsap.from('.list-item', {
  y: 20,
  opacity: 0,
  duration: 0.3,
  stagger: 0.05,
  ease: 'power2.out'
});

ScrollTrigger

import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);

gsap.from('.section', {
  scrollTrigger: {
    trigger: '.section',
    start: 'top 80%',
    end: 'bottom 20%',
    toggleActions: 'play none none reverse'
  },
  y: 50,
  opacity: 0,
  duration: 0.6
});

GSAP Easing

// Power easings (1-4, higher = more dramatic)
ease: 'power1.out'  // Subtle
ease: 'power2.out'  // Standard (like ease-out)
ease: 'power3.out'  // Pronounced
ease: 'power4.out'  // Dramatic

// Special easings
ease: 'back.out(1.7)'   // Overshoot
ease: 'elastic.out(1, 0.3)'  // Bouncy
ease: 'bounce.out'      // Bounce at end

React Integration

import { useGSAP } from '@gsap/react';
import gsap from 'gsap';

function Component() {
  const containerRef = useRef(null);

  useGSAP(() => {
    gsap.from('.item', {
      y: 20,
      opacity: 0,
      stagger: 0.1
    });
  }, { scope: containerRef });

  return (
    <div ref={containerRef}>
      <div className="item">Item 1</div>
      <div className="item">Item 2</div>
    </div>
  );
}

Performance Tips

Use Transform & Opacity

/* Good - GPU accelerated */
transform: translateX(100px);
transform: scale(1.1);
transform: rotate(45deg);
opacity: 0.5;

/* Avoid animating - triggers layout */
width, height, top, left, margin, padding

will-change Hint

/* Use sparingly - only for known animations */
.animated-element {
  will-change: transform, opacity;
}

/* Remove after animation */
.animated-element.done {
  will-change: auto;
}

Reduce Motion Query

// React hook
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

// Framer Motion
<motion.div
  animate={{ x: 100 }}
  transition={{
    duration: prefersReducedMotion ? 0 : 0.3
  }}
/>

Quick Reference

ElementDurationEasingProperties
Button hover150msease-outtransform, shadow, bg
Toggle switch200msease-outtransform
Dropdown open200msease-outopacity, transform
Modal enter250msease-outopacity, scale
Modal exit200msease-inopacity, scale
Page transition300msease-in-outopacity, transform
Toast enter300msspringtransform
Skeleton pulse2000msease-in-outopacity

Motion Checklist

  • Animation has clear purpose (feedback, orientation, focus)
  • Duration feels snappy (not sluggish)
  • Easing matches motion type (ease-out for enters)
  • Respects prefers-reduced-motion
  • Only animates transform/opacity when possible
  • Exit animations are faster than enters
  • Stagger delays are subtle (30-50ms)
  • No animation blocks user interaction