# Animation Patterns Reference
## Table of Contents
1. Easing Functions
2. Page Load Sequences
3. Scroll-Triggered Animations
4. Micro-Interactions
5. Page Transitions
6. Text Animations
7. Performance Optimization
---
## 1. Easing Functions
### CSS Custom Easings (Agency Standards)
```css
:root {
/* Smooth exits - most common for UI */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
--ease-out-circ: cubic-bezier(0, 0.55, 0.45, 1);
/* Dramatic entrances */
--ease-in-expo: cubic-bezier(0.7, 0, 0.84, 0);
--ease-in-quart: cubic-bezier(0.5, 0, 0.75, 0);
/* Symmetric - for toggles, loops */
--ease-in-out-quint: cubic-bezier(0.83, 0, 0.17, 1);
--ease-in-out-expo: cubic-bezier(0.87, 0, 0.13, 1);
/* Bouncy/Elastic */
--ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-out-elastic: cubic-bezier(0.68, -0.6, 0.32, 1.6);
/* Duration standards */
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
--duration-slower: 800ms;
--duration-slowest: 1200ms;
}
```
### GSAP Equivalent
```javascript
// Common GSAP easings
gsap.to(element, {
duration: 1,
ease: "expo.out" // Most used for reveals
});
gsap.to(element, {
duration: 0.8,
ease: "power4.out" // Slightly faster settle
});
gsap.to(element, {
duration: 1.2,
ease: "power2.inOut" // Symmetric movement
});
// Custom ease (Locomotive-style)
gsap.registerPlugin(CustomEase);
CustomEase.create("smooth", "0.16, 1, 0.3, 1");
```
---
## 2. Page Load Sequences
### Staggered Reveal Pattern
```css
/* CSS-only staggered reveal */
.reveal-item {
opacity: 0;
transform: translateY(60px);
animation: revealUp 0.8s var(--ease-out-expo) forwards;
}
.reveal-item:nth-child(1) { animation-delay: 0.1s; }
.reveal-item:nth-child(2) { animation-delay: 0.2s; }
.reveal-item:nth-child(3) { animation-delay: 0.3s; }
.reveal-item:nth-child(4) { animation-delay: 0.4s; }
@keyframes revealUp {
to {
opacity: 1;
transform: translateY(0);
}
}
```
### GSAP Page Load
```javascript
// Orchestrated page load
const tl = gsap.timeline({ defaults: { ease: "expo.out" } });
tl.set(".page-content", { visibility: "visible" })
.from(".hero-title .word", {
y: 120,
opacity: 0,
duration: 1.2,
stagger: 0.1
})
.from(".hero-subtitle", {
y: 40,
opacity: 0,
duration: 0.8
}, "-=0.6")
.from(".hero-cta", {
y: 30,
opacity: 0,
duration: 0.6
}, "-=0.4")
.from(".nav-item", {
y: -20,
opacity: 0,
duration: 0.5,
stagger: 0.05
}, "-=0.8");
```
### Split Text Animation
```javascript
// Using GSAP SplitText or custom splitting
import { SplitText } from "gsap/SplitText";
const split = new SplitText(".hero-title", {
type: "words,chars",
wordsClass: "word",
charsClass: "char"
});
gsap.from(split.chars, {
y: 100,
opacity: 0,
duration: 0.8,
ease: "expo.out",
stagger: 0.02
});
// CSS approach (manual splitting)
/* HTML: He... */
.char {
display: inline-block;
opacity: 0;
transform: translateY(100%);
animation: charReveal 0.6s var(--ease-out-expo) forwards;
}
.char:nth-child(1) { animation-delay: calc(0.03s * 1); }
.char:nth-child(2) { animation-delay: calc(0.03s * 2); }
/* ... generate with CSS custom properties or JS */
```
---
## 3. Scroll-Triggered Animations
### Intersection Observer (Vanilla)
```javascript
// Reusable scroll reveal
const observerOptions = {
root: null,
rootMargin: '0px 0px -100px 0px',
threshold: 0.1
};
const revealOnScroll = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
};
const observer = new IntersectionObserver(revealOnScroll, observerOptions);
document.querySelectorAll('[data-reveal]').forEach(el => {
observer.observe(el);
});
```
```css
/* Accompanying CSS */
[data-reveal] {
opacity: 0;
transform: translateY(60px);
transition: opacity 0.8s var(--ease-out-expo),
transform 0.8s var(--ease-out-expo);
}
[data-reveal].is-visible {
opacity: 1;
transform: translateY(0);
}
/* Stagger children */
[data-reveal="stagger"] > * {
opacity: 0;
transform: translateY(40px);
transition: opacity 0.6s var(--ease-out-expo),
transform 0.6s var(--ease-out-expo);
}
[data-reveal="stagger"].is-visible > *:nth-child(1) { transition-delay: 0.1s; }
[data-reveal="stagger"].is-visible > *:nth-child(2) { transition-delay: 0.2s; }
[data-reveal="stagger"].is-visible > *:nth-child(3) { transition-delay: 0.3s; }
[data-reveal="stagger"].is-visible > * {
opacity: 1;
transform: translateY(0);
}
```
### GSAP ScrollTrigger
```javascript
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
// Fade up on scroll
gsap.utils.toArray('.reveal').forEach(el => {
gsap.from(el, {
y: 60,
opacity: 0,
duration: 1,
ease: "expo.out",
scrollTrigger: {
trigger: el,
start: "top 85%",
toggleActions: "play none none none"
}
});
});
// Parallax effect
gsap.to('.parallax-bg', {
yPercent: -30,
ease: "none",
scrollTrigger: {
trigger: '.parallax-section',
start: "top bottom",
end: "bottom top",
scrub: true
}
});
// Horizontal scroll section
gsap.to('.horizontal-panels', {
xPercent: -100 * (panels.length - 1),
ease: "none",
scrollTrigger: {
trigger: '.horizontal-container',
pin: true,
scrub: 1,
end: () => "+=" + document.querySelector('.horizontal-panels').offsetWidth
}
});
// Pin and reveal
gsap.timeline({
scrollTrigger: {
trigger: '.pinned-section',
start: "top top",
end: "+=200%",
pin: true,
scrub: 1
}
})
.from('.pinned-title', { opacity: 0, y: 100 })
.from('.pinned-content', { opacity: 0, y: 50 }, 0.3);
```
### Lenis Smooth Scroll + GSAP
```javascript
import Lenis from '@studio-freight/lenis';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
// Initialize Lenis
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
direction: 'vertical',
smooth: true,
});
// Connect Lenis to GSAP ScrollTrigger
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
```
---
## 4. Micro-Interactions
### Button Hover States
```css
/* Magnetic button effect */
.magnetic-btn {
position: relative;
transition: transform 0.3s var(--ease-out-expo);
}
/* JS handles the magnetic effect, CSS handles the visual */
.magnetic-btn::before {
content: '';
position: absolute;
inset: 0;
background: var(--color-accent);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.5s var(--ease-out-expo);
}
.magnetic-btn:hover::before {
transform: scaleX(1);
transform-origin: left;
}
/* Text slide effect */
.text-slide-btn {
overflow: hidden;
position: relative;
}
.text-slide-btn span {
display: block;
transition: transform 0.5s var(--ease-out-expo);
}
.text-slide-btn span::after {
content: attr(data-text);
position: absolute;
top: 100%;
left: 0;
}
.text-slide-btn:hover span {
transform: translateY(-100%);
}
```
```javascript
// Magnetic button JS
document.querySelectorAll('.magnetic-btn').forEach(btn => {
btn.addEventListener('mousemove', (e) => {
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
gsap.to(btn, {
x: x * 0.3,
y: y * 0.3,
duration: 0.3,
ease: "power2.out"
});
});
btn.addEventListener('mouseleave', () => {
gsap.to(btn, {
x: 0,
y: 0,
duration: 0.5,
ease: "elastic.out(1, 0.5)"
});
});
});
```
### Link Underline Animations
```css
/* Slide in underline */
.link-underline {
position: relative;
}
.link-underline::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: right;
transition: transform 0.4s var(--ease-out-expo);
}
.link-underline:hover::after {
transform: scaleX(1);
transform-origin: left;
}
/* Draw underline */
.link-draw {
background-image: linear-gradient(currentColor, currentColor);
background-position: 0 100%;
background-repeat: no-repeat;
background-size: 0% 1px;
transition: background-size 0.4s var(--ease-out-expo);
}
.link-draw:hover {
background-size: 100% 1px;
}
```
### Card Hover Effects
```css
/* 3D tilt card */
.tilt-card {
transform-style: preserve-3d;
transition: transform 0.5s var(--ease-out-expo);
}
.tilt-card:hover {
transform: perspective(1000px) rotateX(5deg) rotateY(-5deg);
}
.tilt-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
rgba(255,255,255,0.1) 0%,
transparent 50%
);
opacity: 0;
transition: opacity 0.3s;
}
.tilt-card:hover::before {
opacity: 1;
}
```
---
## 5. Page Transitions
### Basic Fade Transition
```javascript
// Using Barba.js
import barba from '@barba/core';
barba.init({
transitions: [{
name: 'fade',
leave(data) {
return gsap.to(data.current.container, {
opacity: 0,
duration: 0.5,
ease: "power2.inOut"
});
},
enter(data) {
return gsap.from(data.next.container, {
opacity: 0,
duration: 0.5,
ease: "power2.inOut"
});
}
}]
});
```
### Overlay Wipe Transition
```javascript
// Wipe transition with overlay
barba.init({
transitions: [{
name: 'wipe',
leave(data) {
const tl = gsap.timeline();
tl.to('.transition-overlay', {
scaleY: 1,
transformOrigin: 'bottom',
duration: 0.6,
ease: "expo.inOut"
})
.to(data.current.container, {
opacity: 0,
duration: 0.3
}, 0.3);
return tl;
},
enter(data) {
const tl = gsap.timeline();
tl.set(data.next.container, { opacity: 1 })
.to('.transition-overlay', {
scaleY: 0,
transformOrigin: 'top',
duration: 0.6,
ease: "expo.inOut"
})
.from('[data-reveal]', {
y: 60,
opacity: 0,
stagger: 0.1,
duration: 0.8,
ease: "expo.out"
}, 0.2);
return tl;
}
}]
});
```
---
## 6. Text Animations
### Word-by-Word Reveal
```javascript
// Split and animate
function animateText(element) {
const text = element.textContent;
const words = text.split(' ');
element.innerHTML = words.map(word =>
`${word}`
).join(' ');
gsap.from(element.querySelectorAll('.word'), {
y: '100%',
duration: 0.8,
ease: "expo.out",
stagger: 0.05
});
}
```
### Scramble/Decode Effect
```javascript
// Text scramble effect
class TextScramble {
constructor(el) {
this.el = el;
this.chars = '!<>-_\\/[]{}—=+*^?#________';
this.update = this.update.bind(this);
}
setText(newText) {
const oldText = this.el.innerText;
const length = Math.max(oldText.length, newText.length);
const promise = new Promise(resolve => this.resolve = resolve);
this.queue = [];
for (let i = 0; i < length; i++) {
const from = oldText[i] || '';
const to = newText[i] || '';
const start = Math.floor(Math.random() * 40);
const end = start + Math.floor(Math.random() * 40);
this.queue.push({ from, to, start, end });
}
cancelAnimationFrame(this.frameRequest);
this.frame = 0;
this.update();
return promise;
}
update() {
let output = '';
let complete = 0;
for (let i = 0; i < this.queue.length; i++) {
let { from, to, start, end, char } = this.queue[i];
if (this.frame >= end) {
complete++;
output += to;
} else if (this.frame >= start) {
if (!char || Math.random() < 0.28) {
char = this.chars[Math.floor(Math.random() * this.chars.length)];
this.queue[i].char = char;
}
output += char;
} else {
output += from;
}
}
this.el.innerText = output;
if (complete === this.queue.length) {
this.resolve();
} else {
this.frameRequest = requestAnimationFrame(this.update);
this.frame++;
}
}
}
```
---
## 7. Performance Optimization
### GPU-Accelerated Properties
```css
/* ONLY animate these for 60fps */
.performant-animation {
/* Good - GPU accelerated */
transform: translateX(100px);
transform: translateY(100px);
transform: scale(1.2);
transform: rotate(45deg);
opacity: 0.5;
/* Bad - triggers layout/paint */
/* left: 100px; */
/* top: 100px; */
/* width: 200px; */
/* height: 200px; */
/* margin: 20px; */
/* padding: 20px; */
}
/* Force GPU layer */
.will-animate {
will-change: transform, opacity;
transform: translateZ(0);
}
/* Remove will-change after animation */
.animation-complete {
will-change: auto;
}
```
### Reduce Motion Support
```css
@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;
}
}
```
```javascript
// JS check for reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReducedMotion.matches) {
// Skip or simplify animations
gsap.globalTimeline.timeScale(10); // Or skip entirely
}
```
### Lazy Animation Loading
```javascript
// Only initialize animations when needed
const animationObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
initHeavyAnimation(entry.target);
animationObserver.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('[data-heavy-animation]').forEach(el => {
animationObserver.observe(el);
});
```