
Css Animation Creator
- 644 installs
- 237 repo stars
- Updated July 15, 2026
- onewave-ai/claude-skills
css-animation-creator is a Claude agent skill that generates production-grade, accessible, GPU-efficient CSS animations and micro-interactions for developers who need motion design without hand-writing keyframes from scr
About
css-animation-creator is an onewave-ai/claude-skills agent skill for building production-grade web motion using CSS transitions, keyframe animations, Tailwind utilities, and Framer Motion where orchestration is needed. The workflow starts by classifying motion purpose—feedback, delight, guidance, or storytelling—then selects the right technique: CSS transitions for state changes, keyframes for loops, or Framer Motion and JavaScript for scroll-linked and sequenced effects. It emphasizes accessible reduced-motion handling, GPU-friendly transforms, and timing curves that match UI intent. Developers reach for css-animation-creator when adding hover effects, loading spinners, page enter transitions, scroll reveals, or component micro-interactions to React or static frontends. Triggers include tasks mentioning animations, motion design, loading states, or page transitions.
- 7-step workflow that starts with motion purpose and ends with device verification
- Pulls from curated reference files instead of writing keyframes from scratch
- Constrains properties to transform and opacity for GPU acceleration
- Automatically honors reduced-motion preferences on every animation
- Includes Disney 12 principles mapped to UI plus timing and easing tables
Css Animation Creator by the numbers
- 644 all-time installs (skills.sh)
- +21 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #537 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/onewave-ai/claude-skills --skill css-animation-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 644 |
|---|---|
| repo stars | ★ 237 |
| Last updated | July 15, 2026 |
| Repository | onewave-ai/claude-skills ↗ |
How do you build accessible CSS animations for web UI?
Generate production-grade, accessible, and performant CSS animations, transitions, and micro-interactions without starting from a blank canvas.
Who is it for?
Frontend developers adding polished motion to React, Tailwind, or vanilla CSS interfaces who want GPU-efficient, accessible animation patterns.
Skip if: Developers building native mobile animations, game engine timelines, or video compositing pipelines outside browser CSS and Framer Motion.
When should I use this skill?
A UI task needs hover effects, loading states, page transitions, scroll animations, or micro-interactions with accessible motion defaults.
What you get
CSS keyframe blocks, transition rules, Tailwind motion classes, and optional Framer Motion component code with easing and timing notes.
- CSS animation code
- Framer Motion components
- Timing and easing recommendations
By the numbers
- Covers four motion purposes: feedback, delight, guidance, and storytelling
Files
CSS Animation Creator
Build production-grade, accessible, GPU-efficient motion for web UIs using CSS, Tailwind, and Framer Motion.
Workflow
1. Identify the purpose of the motion: feedback, delight, guidance, or storytelling. 2. Choose the technique: CSS transitions for state changes, keyframes for looping or multi-step motion, Framer Motion or JS for orchestration and scroll-linked effects. 3. Set timing and easing to match the interaction. Apply the timing and easing tables in references/principles-and-timing.md. 4. Author the animation. Pull ready-made keyframes and patterns from the reference files below rather than writing from scratch. 5. Constrain animated properties to transform and opacity for GPU acceleration. Apply references/performance.md. 6. Honor reduced-motion preferences for every animation. Apply references/accessibility.md. 7. Verify on low-end devices and confirm no layout thrash before shipping.
Contents
references/principles-and-timing.md— Disney 12 principles mapped to UI, duration and easing tables.references/transitions.md— Transition syntax, easing functions, cubic-bezier presets, Tailwind transitions.references/keyframe-library.md— Keyframe syntax plus fade, scale, bounce, rotate, slide, and attention-seeker keyframes.references/loading-animations.md— Spinners, dots, skeleton loaders, shimmer, progress bars.references/micro-interactions.md— Button, hover, icon, form, and success/error interactions.references/page-transitions.md— View Transitions API, Framer Motion page and shared-layout transitions, staggered lists.references/scroll-animations.md— Intersection Observer, Framer Motion scroll hooks, native CSS scroll-driven animations.references/accessibility.md—prefers-reduced-motionCSS, Tailwind motion-safe/reduce, React hook.references/tailwind-config.md— Drop-intailwind.config.jsanimation and keyframe definitions.references/performance.md— GPU-accelerated properties,will-change,contain, performance checklist.
Accessibility: Reduced Motion
/* Global reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Per-element control */
.animated-element {
animation: bounce 1s infinite;
}
@media (prefers-reduced-motion: reduce) {
.animated-element {
animation: none;
}
}// Tailwind motion-safe/motion-reduce
<div className="motion-safe:animate-bounce motion-reduce:animate-none">
Respects preferences
</div>
// React hook
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReducedMotion(mediaQuery.matches);
const handler = (e) => setPrefersReducedMotion(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
return prefersReducedMotion;
}
// Usage
function AnimatedComponent() {
const prefersReducedMotion = usePrefersReducedMotion();
return (
<motion.div
animate={{ x: 100 }}
transition={{
duration: prefersReducedMotion ? 0 : 0.3
}}
/>
);
}Keyframe Animations
Basic Syntax
@keyframes animationName {
0% { /* starting state */ }
50% { /* midpoint state */ }
100% { /* ending state */ }
}
.element {
animation: animationName 1s ease-in-out infinite;
/* name | duration | timing | iteration-count */
/* Full syntax */
animation-name: animationName;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-delay: 0s;
animation-iteration-count: infinite; /* or number */
animation-direction: normal; /* reverse, alternate, alternate-reverse */
animation-fill-mode: forwards; /* none, forwards, backwards, both */
animation-play-state: running; /* paused */
}Fade Animations
/* Fade In */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Fade In Up */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Fade In Down */
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Fade In Left */
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Fade In Right */
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Fade In Scale */
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Fade Out */
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}Scale Animations
/* Scale In */
@keyframes scaleIn {
from { transform: scale(0); }
to { transform: scale(1); }
}
/* Scale In Bounce */
@keyframes scaleInBounce {
0% { transform: scale(0); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* Pop */
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* Pulse */
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* Heartbeat */
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.3); }
28% { transform: scale(1); }
42% { transform: scale(1.3); }
70% { transform: scale(1); }
}Bounce Animations
/* Bounce */
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-20px); }
60% { transform: translateY(-10px); }
}
/* Bounce In */
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% { transform: scale(0.9); }
100% { transform: scale(1); }
}
/* Bounce In Down */
@keyframes bounceInDown {
0% {
opacity: 0;
transform: translateY(-100px);
}
60% {
opacity: 1;
transform: translateY(20px);
}
80% { transform: translateY(-10px); }
100% { transform: translateY(0); }
}
/* Rubber Band */
@keyframes rubberBand {
0% { transform: scaleX(1); }
30% { transform: scaleX(1.25) scaleY(0.75); }
40% { transform: scaleX(0.75) scaleY(1.25); }
50% { transform: scaleX(1.15) scaleY(0.85); }
65% { transform: scaleX(0.95) scaleY(1.05); }
75% { transform: scaleX(1.05) scaleY(0.95); }
100% { transform: scaleX(1) scaleY(1); }
}Rotate Animations
/* Spin */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Spin Reverse */
@keyframes spinReverse {
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
/* Swing */
@keyframes swing {
20% { transform: rotate(15deg); }
40% { transform: rotate(-10deg); }
60% { transform: rotate(5deg); }
80% { transform: rotate(-5deg); }
100% { transform: rotate(0deg); }
}
/* Wobble */
@keyframes wobble {
0% { transform: translateX(0); }
15% { transform: translateX(-15px) rotate(-5deg); }
30% { transform: translateX(12px) rotate(3deg); }
45% { transform: translateX(-9px) rotate(-3deg); }
60% { transform: translateX(6px) rotate(2deg); }
75% { transform: translateX(-3px) rotate(-1deg); }
100% { transform: translateX(0); }
}
/* Flip */
@keyframes flipX {
0% { transform: perspective(400px) rotateX(90deg); opacity: 0; }
40% { transform: perspective(400px) rotateX(-20deg); }
60% { transform: perspective(400px) rotateX(10deg); opacity: 1; }
80% { transform: perspective(400px) rotateX(-5deg); }
100% { transform: perspective(400px) rotateX(0deg); }
}Slide Animations
/* Slide In Up */
@keyframes slideInUp {
from {
transform: translateY(100%);
visibility: visible;
}
to { transform: translateY(0); }
}
/* Slide In Down */
@keyframes slideInDown {
from {
transform: translateY(-100%);
visibility: visible;
}
to { transform: translateY(0); }
}
/* Slide In Left */
@keyframes slideInLeft {
from {
transform: translateX(-100%);
visibility: visible;
}
to { transform: translateX(0); }
}
/* Slide In Right */
@keyframes slideInRight {
from {
transform: translateX(100%);
visibility: visible;
}
to { transform: translateX(0); }
}
/* Slide Out */
@keyframes slideOutUp {
from { transform: translateY(0); }
to {
transform: translateY(-100%);
visibility: hidden;
}
}Attention Seekers
/* Shake */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
/* Shake Horizontal (stronger) */
@keyframes shakeX {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
20%, 40%, 60%, 80% { transform: translateX(10px); }
}
/* Jello */
@keyframes jello {
0%, 11.1%, 100% { transform: none; }
22.2% { transform: skewX(-12.5deg) skewY(-12.5deg); }
33.3% { transform: skewX(6.25deg) skewY(6.25deg); }
44.4% { transform: skewX(-3.125deg) skewY(-3.125deg); }
55.5% { transform: skewX(1.5625deg) skewY(1.5625deg); }
66.6% { transform: skewX(-0.78125deg) skewY(-0.78125deg); }
77.7% { transform: skewX(0.390625deg) skewY(0.390625deg); }
88.8% { transform: skewX(-0.1953125deg) skewY(-0.1953125deg); }
}
/* Flash */
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0; }
}
/* Tada */
@keyframes tada {
0% { transform: scale(1) rotate(0); }
10%, 20% { transform: scale(0.9) rotate(-3deg); }
30%, 50%, 70%, 90% { transform: scale(1.1) rotate(3deg); }
40%, 60%, 80% { transform: scale(1.1) rotate(-3deg); }
100% { transform: scale(1) rotate(0); }
}Loading Animations
Spinners
// Simple spinner
<div className="w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin" />
// Dual ring
<div className="relative w-12 h-12">
<div className="absolute inset-0 border-4 border-blue-200 rounded-full" />
<div className="absolute inset-0 border-4 border-transparent border-t-blue-600 rounded-full animate-spin" />
</div>
// Gradient spinner
<div className="w-10 h-10 rounded-full animate-spin"
style={{
background: 'conic-gradient(from 0deg, transparent, #3b82f6)',
mask: 'radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px))'
}}
/>/* Pulsing ring */
@keyframes pingRing {
0% {
transform: scale(1);
opacity: 1;
}
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
.ping-ring {
position: relative;
}
.ping-ring::before {
content: '';
position: absolute;
inset: 0;
border: 2px solid currentColor;
border-radius: 50%;
animation: pingRing 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
}Dots Loading
// Bouncing dots
<div className="flex gap-1">
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.3s]" />
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.15s]" />
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" />
</div>
// Pulsing dots
<div className="flex gap-1">
{[0, 1, 2].map((i) => (
<div
key={i}
className="w-2 h-2 bg-blue-600 rounded-full animate-pulse"
style={{ animationDelay: `${i * 0.15}s` }}
/>
))}
</div>/* Scaling dots */
@keyframes dotScale {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.dot-loader {
display: flex;
gap: 4px;
}
.dot-loader span {
width: 8px;
height: 8px;
background: currentColor;
border-radius: 50%;
animation: dotScale 1.4s ease-in-out infinite;
}
.dot-loader span:nth-child(1) { animation-delay: -0.32s; }
.dot-loader span:nth-child(2) { animation-delay: -0.16s; }
.dot-loader span:nth-child(3) { animation-delay: 0s; }Skeleton Loaders
// Basic skeleton
<div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
// Card skeleton
<div className="animate-pulse">
<div className="bg-gray-200 h-48 rounded-t-lg" />
<div className="p-4 space-y-3">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
</div>
</div>
// Shimmer effect
<div className="relative overflow-hidden bg-gray-200 rounded">
<div className="absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-white/60 to-transparent" />
</div>/* Shimmer keyframe */
@keyframes shimmer {
100% { transform: translateX(100%); }
}Progress Bars
// Indeterminate progress
<div className="h-1 w-full bg-gray-200 rounded overflow-hidden">
<div className="h-full bg-blue-600 w-1/3 animate-[progress_1s_ease-in-out_infinite]" />
</div>
// Striped progress
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-blue-600 transition-all duration-300"
style={{
width: `${progress}%`,
backgroundImage: 'linear-gradient(45deg, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%)',
backgroundSize: '1rem 1rem',
animation: 'progress-stripes 1s linear infinite'
}}
/>
</div>@keyframes progress {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
@keyframes progress-stripes {
from { background-position: 1rem 0; }
to { background-position: 0 0; }
}Micro-interactions
Button Effects
// Press effect
<button className="transition-transform duration-100 active:scale-95">
Click me
</button>
// Ripple effect (React)
function RippleButton({ children, ...props }) {
const [ripples, setRipples] = useState([]);
const handleClick = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setRipples([...ripples, { x, y, id: Date.now() }]);
setTimeout(() => setRipples(r => r.slice(1)), 600);
};
return (
<button className="relative overflow-hidden" onClick={handleClick} {...props}>
{ripples.map(ripple => (
<span
key={ripple.id}
className="absolute bg-white/30 rounded-full animate-[ripple_0.6s_ease-out]"
style={{
left: ripple.x,
top: ripple.y,
transform: 'translate(-50%, -50%)'
}}
/>
))}
{children}
</button>
);
}@keyframes ripple {
from {
width: 0;
height: 0;
opacity: 0.5;
}
to {
width: 200px;
height: 200px;
opacity: 0;
}
}Hover Effects
// Lift effect
<div className="transition-all duration-300 hover:-translate-y-1 hover:shadow-lg">
Card content
</div>
// Glow effect
<button className="transition-shadow duration-300 hover:shadow-[0_0_20px_rgba(59,130,246,0.5)]">
Glow button
</button>
// Border animation
<div className="relative group">
<div className="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-cyan-600 rounded-lg blur opacity-0 group-hover:opacity-75 transition duration-300" />
<div className="relative bg-white rounded-lg p-6">Content</div>
</div>
// Underline animation
<a className="relative inline-block after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 after:bg-current after:transition-all after:duration-300 hover:after:w-full">
Animated link
</a>
// Fill animation
<a className="relative overflow-hidden group">
<span className="relative z-10 transition-colors duration-300 group-hover:text-white">
Hover me
</span>
<span className="absolute inset-0 bg-blue-600 transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300" />
</a>Icon Animations
// Rotate on hover
<button className="group">
<SettingsIcon className="transition-transform duration-500 group-hover:rotate-180" />
</button>
// Bounce on hover
<button className="group">
<ArrowIcon className="transition-transform group-hover:translate-x-1 group-hover:animate-bounce" />
</button>
// Scale + rotate
<button className="group">
<PlusIcon className="transition-all duration-300 group-hover:scale-110 group-hover:rotate-90" />
</button>Form Interactions
// Input focus effect
<div className="relative">
<input
className="peer w-full border-b-2 border-gray-300 focus:border-blue-600 outline-none py-2 transition-colors"
placeholder=" "
/>
<label className="absolute left-0 top-2 text-gray-500 transition-all peer-focus:-top-4 peer-focus:text-sm peer-focus:text-blue-600 peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-sm">
Email
</label>
</div>
// Checkbox animation
<label className="flex items-center gap-2 cursor-pointer">
<div className="relative">
<input type="checkbox" className="peer sr-only" />
<div className="w-5 h-5 border-2 rounded transition-colors peer-checked:bg-blue-600 peer-checked:border-blue-600" />
<CheckIcon className="absolute inset-0 m-auto w-3 h-3 text-white opacity-0 scale-0 transition-all peer-checked:opacity-100 peer-checked:scale-100" />
</div>
Label text
</label>
// Toggle switch
<button
role="switch"
aria-checked={enabled}
onClick={() => setEnabled(!enabled)}
className={cn(
"relative w-11 h-6 rounded-full transition-colors",
enabled ? "bg-blue-600" : "bg-gray-200"
)}
>
<span className={cn(
"absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform",
enabled && "translate-x-5"
)} />
</button>Success/Error States
// Success checkmark
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
<svg className="w-8 h-8 text-green-600" viewBox="0 0 24 24">
<path
className="animate-[draw_0.5s_ease-out_forwards]"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="24"
strokeDashoffset="24"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
// Error shake
<input className="animate-[shake_0.5s_ease-in-out] border-red-500" />@keyframes draw {
to { stroke-dashoffset: 0; }
}Page Transitions
CSS-only Transitions
/* View Transitions API (Chrome 111+) */
@view-transition {
navigation: auto;
}
::view-transition-old(root) {
animation: fadeOut 0.3s ease-out;
}
::view-transition-new(root) {
animation: fadeIn 0.3s ease-in;
}
/* Specific element transitions */
.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: 0.5s;
}Framer Motion
import { motion, AnimatePresence } from 'framer-motion';
// Fade transition
const pageVariants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
function PageWrapper({ children }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
);
}
// Slide transition
const slideVariants = {
initial: { opacity: 0, x: 20 },
animate: { opacity: 1, x: 0 },
exit: { opacity: 0, x: -20 },
};
// Scale + fade
const scaleVariants = {
initial: { opacity: 0, scale: 0.95 },
animate: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 1.05 },
};
// Shared layout animations
function Gallery({ items, selectedId }) {
return (
<>
{items.map(item => (
<motion.div key={item.id} layoutId={item.id}>
<img src={item.src} />
</motion.div>
))}
<AnimatePresence>
{selectedId && (
<motion.div layoutId={selectedId} className="modal">
<img src={items.find(i => i.id === selectedId).src} />
</motion.div>
)}
</AnimatePresence>
</>
);
}Staggered Animations
// Framer Motion stagger
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
function StaggeredList({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
);
}
// CSS stagger with custom properties
<ul className="stagger-list">
{items.map((item, i) => (
<li
key={item.id}
style={{ '--i': i } as React.CSSProperties}
className="animate-fadeInUp opacity-0"
>
{item.name}
</li>
))}
</ul>.stagger-list li {
animation: fadeInUp 0.5s ease forwards;
animation-delay: calc(var(--i) * 0.1s);
}Performance Best Practices
GPU-Accelerated Properties
/* GOOD - GPU accelerated */
transform: translateX(100px);
transform: scale(1.1);
transform: rotate(45deg);
opacity: 0.5;
/* BAD - Triggers layout/paint */
left: 100px;
top: 50px;
width: 200px;
height: 100px;
margin: 20px;
padding: 10px;
border-width: 2px;
font-size: 16px;will-change (Use Sparingly)
/* Only when needed for complex animations */
.complex-animation {
will-change: transform, opacity;
}
/* Remove after animation */
.complex-animation.done {
will-change: auto;
}Contain for Isolation
.animated-section {
contain: layout style paint;
}Animation Performance Checklist
- [ ] Only animate
transformandopacity - [ ] Use
will-changeonly when necessary - [ ] Keep animations under 300ms for UI feedback
- [ ] Test on low-end devices
- [ ] Use
containfor isolated sections - [ ] Reduce animation during scroll
- [ ] Pause off-screen animations
Animation Principles and Timing
The 12 Principles (Disney) Applied to UI
| Principle | UI Application |
|---|---|
| Squash & Stretch | Button press, elastic effects |
| Anticipation | Hover states before action |
| Staging | Focus attention on important elements |
| Follow Through | Overshoot then settle |
| Ease In/Out | Natural acceleration/deceleration |
| Arcs | Curved motion paths |
| Secondary Action | Supporting animations |
| Timing | Duration conveys weight/importance |
| Exaggeration | Emphasis for clarity |
| Appeal | Pleasing, polished motion |
Timing Guidelines
| Animation Type | Duration | Easing |
|---|---|---|
| Micro-interaction | 100-200ms | ease-out |
| Button/hover | 150-250ms | ease |
| Modal open | 200-300ms | ease-out |
| Modal close | 150-200ms | ease-in |
| Page transition | 300-500ms | ease-in-out |
| Loading loop | 1000-2000ms | linear/ease-in-out |
| Attention grab | 500-1000ms | elastic |
Scroll Animations
Intersection Observer
function useInView(options = {}) {
const ref = useRef(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
if (options.once) observer.disconnect();
}
}, { threshold: 0.1, ...options });
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return [ref, isInView];
}
// Usage
function AnimatedSection() {
const [ref, isInView] = useInView({ once: true });
return (
<div
ref={ref}
className={cn(
"transition-all duration-700",
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
)}
>
Content
</div>
);
}Scroll-triggered with Framer Motion
import { motion, useScroll, useTransform } from 'framer-motion';
function ParallaxSection() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
});
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
return (
<motion.div ref={ref} style={{ y, opacity }}>
Parallax content
</motion.div>
);
}
// Scroll-linked progress
function ScrollProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-600 origin-left"
style={{ scaleX: scrollYProgress }}
/>
);
}CSS Scroll-driven Animations
/* Native scroll-driven animations (Chrome 115+) */
@keyframes reveal {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.scroll-reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
/* Scroll progress indicator */
.progress-bar {
transform-origin: left;
animation: grow linear;
animation-timeline: scroll();
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}Tailwind Animation Config
// tailwind.config.js
module.exports = {
theme: {
extend: {
animation: {
// Fade
'fade-in': 'fadeIn 0.5s ease forwards',
'fade-in-up': 'fadeInUp 0.5s ease forwards',
'fade-in-down': 'fadeInDown 0.5s ease forwards',
'fade-out': 'fadeOut 0.3s ease forwards',
// Slide
'slide-in-left': 'slideInLeft 0.3s ease-out',
'slide-in-right': 'slideInRight 0.3s ease-out',
'slide-in-up': 'slideInUp 0.3s ease-out',
'slide-in-down': 'slideInDown 0.3s ease-out',
// Scale
'scale-in': 'scaleIn 0.2s ease-out',
'pop': 'pop 0.3s ease-out',
// Attention
'shake': 'shake 0.5s ease-in-out',
'wiggle': 'wiggle 1s ease-in-out infinite',
'heartbeat': 'heartbeat 1.5s ease-in-out infinite',
// Loading
'shimmer': 'shimmer 2s infinite',
'progress': 'progress 1s ease-in-out infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
fadeInUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
fadeInDown: {
'0%': { opacity: '0', transform: 'translateY(-20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
fadeOut: {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
slideInLeft: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(0)' },
},
slideInRight: {
'0%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(0)' },
},
slideInUp: {
'0%': { transform: 'translateY(100%)' },
'100%': { transform: 'translateY(0)' },
},
slideInDown: {
'0%': { transform: 'translateY(-100%)' },
'100%': { transform: 'translateY(0)' },
},
scaleIn: {
'0%': { transform: 'scale(0)' },
'100%': { transform: 'scale(1)' },
},
pop: {
'0%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.1)' },
'100%': { transform: 'scale(1)' },
},
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'10%, 30%, 50%, 70%, 90%': { transform: 'translateX(-5px)' },
'20%, 40%, 60%, 80%': { transform: 'translateX(5px)' },
},
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' },
},
heartbeat: {
'0%, 100%': { transform: 'scale(1)' },
'14%': { transform: 'scale(1.3)' },
'28%': { transform: 'scale(1)' },
'42%': { transform: 'scale(1.3)' },
'70%': { transform: 'scale(1)' },
},
shimmer: {
'100%': { transform: 'translateX(100%)' },
},
progress: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(400%)' },
},
},
},
},
};CSS Transitions
Basic Syntax
.element {
/* Single property */
transition: opacity 0.3s ease;
/* Multiple properties */
transition:
transform 0.3s ease,
opacity 0.3s ease,
background-color 0.2s ease;
/* Shorthand: property duration timing-function delay */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0s;
}Easing Functions
/* Built-in */
transition-timing-function: linear;
transition-timing-function: ease; /* Default - slow start, fast middle, slow end */
transition-timing-function: ease-in; /* Slow start */
transition-timing-function: ease-out; /* Slow end */
transition-timing-function: ease-in-out; /* Slow start and end */
/* Custom cubic-bezier */
/* Material Design standard */
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
/* Decelerate (entering) */
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
/* Accelerate (exiting) */
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
/* Bounce effect */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Elastic */
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);Tailwind Transitions
// Duration
<div className="transition duration-150" /> // 150ms
<div className="transition duration-300" /> // 300ms
<div className="transition duration-500" /> // 500ms
// Timing function
<div className="transition ease-linear" />
<div className="transition ease-in" />
<div className="transition ease-out" />
<div className="transition ease-in-out" />
// Specific properties (better performance)
<div className="transition-opacity" />
<div className="transition-transform" />
<div className="transition-colors" />
<div className="transition-shadow" />
<div className="transition-all" />
// Combined
<button className="transition-all duration-200 ease-out hover:scale-105 hover:shadow-lg">
Hover me
</button>Related skills
How it compares
Pick css-animation-creator over generic frontend skills when the task is specifically motion timing, easing, and accessible CSS animation patterns.
FAQ
What animation techniques does css-animation-creator support?
css-animation-creator supports CSS transitions for state changes, CSS keyframes for looping motion, Tailwind utility classes, and Framer Motion or JavaScript for orchestrated and scroll-linked effects.
When should I use Framer Motion vs pure CSS?
css-animation-creator recommends pure CSS for simple state changes and loops, and Framer Motion when scroll-linked sequencing, layout animation, or multi-step orchestration is required in React components.