Animation Patterns
Common animation patterns and techniques with typestyles
Animation Patterns
TypeStyles supports CSS animations through the keyframes API. This guide covers common animation patterns and best practices.
Basic animations
Fade in
import { keyframes, styles } from 'typestyles';
const fadeIn = keyframes.create('fadeIn', {
from: { opacity: 0 },
to: { opacity: 1 },
});
const card = styles.create('card', {
base: {
animation: `${fadeIn} 300ms ease`,
},
});
Slide in
const slideInRight = keyframes.create('slideInRight', {
from: {
opacity: 0,
transform: 'translateX(20px)',
},
to: {
opacity: 1,
transform: 'translateX(0)',
},
});
const slideInLeft = keyframes.create('slideInLeft', {
from: {
opacity: 0,
transform: 'translateX(-20px)',
},
to: {
opacity: 1,
transform: 'translateX(0)',
},
});
const slideInUp = keyframes.create('slideInUp', {
from: {
opacity: 0,
transform: 'translateY(20px)',
},
to: {
opacity: 1,
transform: 'translateY(0)',
},
});
const slideInDown = keyframes.create('slideInDown', {
from: {
opacity: 0,
transform: 'translateY(-20px)',
},
to: {
opacity: 1,
transform: 'translateY(0)',
},
});
Scale animations
const scaleIn = keyframes.create('scaleIn', {
from: {
opacity: 0,
transform: 'scale(0.9)',
},
to: {
opacity: 1,
transform: 'scale(1)',
},
});
const scaleOut = keyframes.create('scaleOut', {
from: {
opacity: 1,
transform: 'scale(1)',
},
to: {
opacity: 0,
transform: 'scale(0.9)',
},
});
const popIn = keyframes.create('popIn', {
'0%': { transform: 'scale(0)' },
'70%': { transform: 'scale(1.1)' },
'100%': { transform: 'scale(1)' },
});
Common UI animations
Loading spinner
const spin = keyframes.create('spin', {
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' },
});
const spinner = styles.create('spinner', {
base: {
display: 'inline-block',
width: '24px',
height: '24px',
border: '3px solid #e5e7eb',
borderTopColor: '#3b82f6',
borderRadius: '50%',
animation: `${spin} 800ms linear infinite`,
},
sm: {
width: '16px',
height: '16px',
borderWidth: '2px',
},
lg: {
width: '32px',
height: '32px',
borderWidth: '4px',
},
});
Pulse animation
const pulse = keyframes.create('pulse', {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.5 },
});
const skeleton = styles.create('skeleton', {
base: {
backgroundColor: '#e5e7eb',
borderRadius: '4px',
animation: `${pulse} 2s ease-in-out infinite`,
},
});
Shake animation (error state)
const shake = keyframes.create('shake', {
'0%, 100%': { transform: 'translateX(0)' },
'10%, 30%, 50%, 70%, 90%': { transform: 'translateX(-4px)' },
'20%, 40%, 60%, 80%': { transform: 'translateX(4px)' },
});
const input = styles.create('input', {
base: { ... },
error: {
borderColor: '#ef4444',
animation: `${shake} 500ms ease-in-out`,
},
});
Bounce animation
const bounce = keyframes.create('bounce', {
'0%, 100%': {
transform: 'translateY(0)',
animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)',
},
'50%': {
transform: 'translateY(-25%)',
animationTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
},
});
const notification = styles.create('notification', {
badge: {
animation: `${bounce} 1s infinite`,
},
});
Page transitions
Page fade
const pageFadeIn = keyframes.create('pageFadeIn', {
from: { opacity: 0 },
to: { opacity: 1 },
});
const pageLayout = styles.create('page', {
container: {
animation: `${pageFadeIn} 300ms ease`,
},
});
Staggered list items
// Individual item animation
const listItemFadeIn = keyframes.create('listItemFadeIn', {
from: {
opacity: 0,
transform: 'translateY(10px)',
},
to: {
opacity: 1,
transform: 'translateY(0)',
},
});
// Delay classes for stagger effect
const list = styles.create('list', {
item: {
opacity: 0,
animation: `${listItemFadeIn} 300ms ease forwards`,
},
itemDelay1: { animationDelay: '50ms' },
itemDelay2: { animationDelay: '100ms' },
itemDelay3: { animationDelay: '150ms' },
itemDelay4: { animationDelay: '200ms' },
itemDelay5: { animationDelay: '250ms' },
});
// React usage with index
function List({ items }) {
return (
<ul>
{items.map((item, index) => (
<li
key={item.id}
className={list(
'item',
index === 0 && 'itemDelay1',
index === 1 && 'itemDelay2',
index === 2 && 'itemDelay3',
// ... or use a function to calculate delay
)}
>
{item.name}
</li>
))}
</ul>
);
}
Micro-interactions
Button press
const buttonPress = keyframes.create('buttonPress', {
'0%': { transform: 'scale(1)' },
'50%': { transform: 'scale(0.95)' },
'100%': { transform: 'scale(1)' },
});
const button = styles.create('button', {
base: {
transition: 'transform 100ms ease',
'&:active': {
transform: 'scale(0.95)',
},
},
// Or with keyframe for more control
press: {
animation: `${buttonPress} 150ms ease`,
},
});
Checkbox check
const checkmark = keyframes.create('checkmark', {
'0%': {
strokeDashoffset: 100,
},
'100%': {
strokeDashoffset: 0,
},
});
const checkbox = styles.create('checkbox', {
base: { ... },
checkmark: {
strokeDasharray: 100,
strokeDashoffset: 100,
},
checked: {
'& .checkmark': {
animation: `${checkmark} 200ms ease forwards`,
},
},
});
Hover lift effect
const card = styles.create('card', {
base: {
transition: 'transform 200ms ease, box-shadow 200ms ease',
},
interactive: {
cursor: 'pointer',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 12px 24px rgba(0, 0, 0, 0.15)',
},
},
});
Complex animations
Modal enter/exit
const modalBackdropFadeIn = keyframes.create('modalBackdropFadeIn', {
from: { opacity: 0 },
to: { opacity: 1 },
});
const modalBackdropFadeOut = keyframes.create('modalBackdropFadeOut', {
from: { opacity: 1 },
to: { opacity: 0 },
});
const modalContentSlideIn = keyframes.create('modalContentSlideIn', {
from: {
opacity: 0,
transform: 'scale(0.95) translateY(10px)',
},
to: {
opacity: 1,
transform: 'scale(1) translateY(0)',
},
});
const modalContentSlideOut = keyframes.create('modalContentSlideOut', {
from: {
opacity: 1,
transform: 'scale(1) translateY(0)',
},
to: {
opacity: 0,
transform: 'scale(0.95) translateY(10px)',
},
});
const modal = styles.create('modal', {
backdrop: {
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
backdropEnter: {
animation: `${modalBackdropFadeIn} 200ms ease`,
},
backdropExit: {
animation: `${modalBackdropFadeOut} 200ms ease forwards`,
},
content: {
backgroundColor: 'white',
borderRadius: '8px',
padding: '24px',
},
contentEnter: {
animation: `${modalContentSlideIn} 300ms ease`,
},
contentExit: {
animation: `${modalContentSlideOut} 200ms ease forwards`,
},
});
Toast notifications
const toastSlideIn = keyframes.create('toastSlideIn', {
from: {
opacity: 0,
transform: 'translateX(100%)',
},
to: {
opacity: 1,
transform: 'translateX(0)',
},
});
const toastSlideOut = keyframes.create('toastSlideOut', {
from: {
opacity: 1,
transform: 'translateX(0)',
},
to: {
opacity: 0,
transform: 'translateX(100%)',
},
});
const toastProgress = keyframes.create('toastProgress', {
from: { transform: 'scaleX(1)' },
to: { transform: 'scaleX(0)' },
});
const toast = styles.create('toast', {
base: {
padding: '16px 20px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
},
enter: {
animation: `${toastSlideIn} 300ms ease`,
},
exit: {
animation: `${toastSlideOut} 300ms ease forwards`,
},
progressBar: {
height: '3px',
backgroundColor: 'rgba(255, 255, 255, 0.3)',
marginTop: '12px',
transformOrigin: 'left',
animation: `${toastProgress} 5000ms linear forwards`,
},
success: {
backgroundColor: '#10b981',
color: 'white',
},
error: {
backgroundColor: '#ef4444',
color: 'white',
},
info: {
backgroundColor: '#3b82f6',
color: 'white',
},
});
Skeleton loading
const shimmer = keyframes.create('shimmer', {
'0%': { backgroundPosition: '-200% 0' },
'100%': { backgroundPosition: '200% 0' },
});
const skeleton = styles.create('skeleton', {
base: {
backgroundColor: '#e5e7eb',
borderRadius: '4px',
},
animated: {
background: 'linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%)',
backgroundSize: '200% 100%',
animation: `${shimmer} 1.5s infinite`,
},
text: {
height: '1em',
marginBottom: '0.5em',
},
textLarge: {
height: '1.5em',
},
circle: {
borderRadius: '50%',
},
avatar: {
width: '40px',
height: '40px',
},
card: {
height: '200px',
},
});
Scroll animations
Infinite scroll indicator
const scrollIndicator = keyframes.create('scrollIndicator', {
'0%, 100%': {
opacity: 1,
transform: 'translateY(0)',
},
'50%': {
opacity: 0.5,
transform: 'translateY(6px)',
},
});
const scrollCue = styles.create('scroll-cue', {
base: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4px',
},
arrow: {
animation: `${scrollIndicator} 1.5s ease-in-out infinite`,
},
arrowDelay1: { animationDelay: '0ms' },
arrowDelay2: { animationDelay: '150ms' },
arrowDelay3: { animationDelay: '300ms' },
});
Best practices
Performance
// ✅ Use transform and opacity for smooth animations
const goodAnimation = keyframes.create('goodAnimation', {
from: {
opacity: 0,
transform: 'translateY(20px)', // GPU accelerated
},
to: {
opacity: 1,
transform: 'translateY(0)',
},
});
// ❌ Avoid animating layout properties
const badAnimation = keyframes.create('badAnimation', {
from: {
height: 0, // Triggers layout
marginTop: 0, // Triggers layout
},
to: {
height: '100px',
marginTop: '20px',
},
});
Accessibility
// Respect reduced motion preference
const animated = styles.create('animated', {
base: {
'@media (prefers-reduced-motion: reduce)': {
animation: 'none',
transition: 'none',
},
},
});
Timing functions
// Standard curves
const ease = 'ease'; // Standard
const easeIn = 'ease-in'; // Accelerate
const easeOut = 'ease-out'; // Decelerate
const easeInOut = 'ease-in-out'; // Both
// Custom cubic-bezier for specific feels
const springy = 'cubic-bezier(0.68, -0.55, 0.265, 1.55)';
const smooth = 'cubic-bezier(0.4, 0, 0.2, 1)';
const enter = 'cubic-bezier(0, 0, 0.2, 1)';
const exit = 'cubic-bezier(0.4, 0, 1, 1)';
// Usage
const button = styles.create('button', {
base: {
transition: `transform 200ms ${springy}`,
},
});
Duration guidelines
| Animation Type | Duration | Use Case |
|---|---|---|
| Micro-interaction | 100-200ms | Button press, hover states |
| Standard | 200-300ms | Modals, dropdowns, tooltips |
| Complex | 300-500ms | Page transitions, reveals |
| Ambient | 1-10s | Loading states, skeletons |
Naming conventions
// Use descriptive names
const fadeIn = keyframes.create('fadeIn', { ... });
const slideUp = keyframes.create('slideUp', { ... });
const modalEnter = keyframes.create('modalEnter', { ... });
const modalExit = keyframes.create('modalExit', { ... });
// Include direction when applicable
const slideInRight = keyframes.create('slideInRight', { ... });
const slideInLeft = keyframes.create('slideInLeft', { ... });
const slideOutUp = keyframes.create('slideOutUp', { ... });
Animation utilities
Animation delay helper
function createStaggeredAnimation(
baseName: string,
itemCount: number,
baseDelay: number
): Record<string, string> {
const delays: Record<string, string> = {};
for (let i = 0; i < itemCount; i++) {
delays[`delay${i + 1}`] = `${baseDelay * (i + 1)}ms`;
}
return delays;
}
// Usage in React
function StaggeredList({ items }) {
const delayClass = (index: number) => {
const delays = ['delay1', 'delay2', 'delay3', 'delay4', 'delay5'];
return delays[index] || 'delay5';
};
return (
<ul>
{items.map((item, index) => (
<li
key={item.id}
className={list('item', delayClass(index))}
>
{item.name}
</li>
))}
</ul>
);
}
Animation presets
// animations/presets.ts
export const presets = {
fade: {
in: 'fadeIn 300ms ease',
out: 'fadeOut 200ms ease',
},
slide: {
up: 'slideUp 300ms ease',
down: 'slideDown 300ms ease',
left: 'slideLeft 300ms ease',
right: 'slideRight 300ms ease',
},
scale: {
in: 'scaleIn 200ms ease',
out: 'scaleOut 200ms ease',
},
bounce: 'bounce 500ms ease',
pulse: 'pulse 2s ease-in-out infinite',
spin: 'spin 1s linear infinite',
shake: 'shake 500ms ease-in-out',
} as const;
// Usage
const modal = styles.create('modal', {
enter: {
animation: presets.fade.in,
},
});
Summary
- Use transform and opacity - Best performance
- Respect reduced motion - Accessibility first
- Keep animations purposeful - Don't animate just because you can
- Use appropriate timing - Match animation to importance
- Consider physics - Natural motion feels better
- Test on real devices - Performance varies
- Name descriptively - Make intent clear
- Reuse animations - Create a preset library
- Document timing - Help other developers
- Test with users - Get feedback on feel