一键导入
css-animation-creator
// Create professional CSS animations, transitions, micro-interactions, and complex motion design. Use when adding animations, hover effects, loading states, page transitions, scroll animations, or any motion design work.
// Create professional CSS animations, transitions, micro-interactions, and complex motion design. Use when adding animations, hover effects, loading states, page transitions, scroll animations, or any motion design work.
HyperFrames CLI dev loop — use `npx hyperframes` for scaffolding (init), validation (lint, inspect), preview, render, and environment troubleshooting (doctor, browser, info, upgrade). Use when working in or alongside a HyperFrames project and the user asks to scaffold, preview, render, lint, install a registry block, or diagnose a broken environment.
Asset preprocessing for HyperFrames compositions — text-to-speech narration (Kokoro), audio/video transcription (Whisper), and background removal for transparent overlays. Use when a HyperFrames project needs a voiceover, captions/subtitles from existing audio, or a clean cutout from a photo/video for use as an overlay.
Capture a website and create a HyperFrames video from it. Use whenever the user (1) provides a URL and wants a video, (2) says "capture this site", "turn this into a video", "make a video tour of this page", or (3) wants a scrolling product walkthrough, marketing reel, or before/after visual built from a real site.
Create video compositions, animations, title cards, overlays, captions, voiceovers, audio-reactive visuals, and scene transitions. Use whenever the user asks to build a video, motion graphic, animated explainer, intro, outro, title card, or convert a website / podcast / talk into a video. HyperFrames is HTML-based — fast iteration, real rendering.
Audit websites for accessibility issues and WCAG compliance. Use when checking accessibility, fixing a11y issues, or ensuring WCAG compliance.
Generate REST API endpoints with proper structure, validation, error handling, and types. Use when creating new API routes, endpoints, or backend services.
| name | css-animation-creator |
| description | Create professional CSS animations, transitions, micro-interactions, and complex motion design. Use when adding animations, hover effects, loading states, page transitions, scroll animations, or any motion design work. |
When creating animations:
| 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 |
| 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 |
.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;
}
/* 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);
// 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>
@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 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 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 */
@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); }
}
/* 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 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;
}
}
/* 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); }
}
// 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;
}
// 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; }
// 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%); }
}
// 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; }
}
// 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;
}
}
// 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-pink-600 to-purple-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>
// 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>
// 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 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; }
}
/* 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;
}
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>
</>
);
}
// 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);
}
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>
);
}
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 }}
/>
);
}
/* 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); }
}
/* 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
}}
/>
);
}
// 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%)' },
},
},
},
},
};
/* 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;
/* Only when needed for complex animations */
.complex-animation {
will-change: transform, opacity;
}
/* Remove after animation */
.complex-animation.done {
will-change: auto;
}
.animated-section {
contain: layout style paint;
}
transform and opacitywill-change only when necessarycontain for isolated sections