gsap-flip
Installation
SKILL.md
GSAP Flip - Layout Animations
Flip (First, Last, Invert, Play) creates smooth transitions between DOM states. Perfect for layout changes, reordering, size transitions, and responsive animations without jumps.
Installation
npm install gsap
import { Flip } from 'gsap/Flip'
gsap.registerPlugin(Flip)
Basic FLIP Pattern
Simple Flip
gsap.registerPlugin(Flip)
// 1. Capture initial state
const state = Flip.getState('.box')
// 2. Make DOM changes
document.querySelector('.box').classList.add('moved')
// 3. Animate from old to new position
Flip.from(state, {
duration: 1,
ease: 'power2.out'
})
Class Toggle
const box = document.querySelector('.box')
const state = Flip.getState(box)
box.classList.toggle('active')
Flip.from(state, {
duration: 0.8,
ease: 'power2.inOut'
})
Position Change
// Capture state
const state = Flip.getState('.item')
// Move element
const item = document.querySelector('.item')
item.style.transform = 'translateX(200px)'
// Animate transition
Flip.from(state, {
duration: 1
})
Absolute Positioning
Basic Absolute
const state = Flip.getState('.box')
document.querySelector('.box').classList.add('new-position')
Flip.from(state, {
duration: 1,
absolute: true // Apply position: absolute during animation
})
Absolute with Selector
const state = Flip.getState('.target')
// Change layout
document.querySelector('.container').classList.add('rearranged')
Flip.from(state, {
duration: 1,
absolute: '.target' // Apply absolute to specific elements
})
Absolute on Leave
const state = Flip.getState('.item')
// Remove from DOM
document.querySelector('.item').remove()
Flip.from(state, {
duration: 0.8,
absoluteOnLeave: true // Keep visible during exit animation
})
Fade Effects
Cross Fade
// Add matching IDs to swapped elements
const items = document.querySelectorAll('.item')
items.forEach((item, i) => {
item.setAttribute('data-flip-id', `item-${i}`)
})
const state = Flip.getState('.item')
// Swap elements
const container = document.querySelector('.container')
container.insertBefore(container.children[2], container.children[0])
Flip.from(state, {
duration: 1,
fade: true, // Enable cross-fade
ease: 'power2.inOut'
})
Fade Only
const state = Flip.getState('.box')
document.querySelector('.box').classList.add('hidden')
Flip.from(state, {
duration: 0.5,
fade: true,
absolute: true
})
Scale vs Width/Height
Scale Animation
const state = Flip.getState('.box')
document.querySelector('.box').classList.add('larger')
Flip.from(state, {
duration: 1,
scale: true, // Use scale instead of width/height
ease: 'power2.out'
})
Width/Height Animation
const state = Flip.getState('.box')
document.querySelector('.box').classList.add('larger')
Flip.from(state, {
duration: 1,
// Default: animates width/height
ease: 'power2.out'
})
Reordering
Grid Reorder
const container = document.querySelector('.grid')
const state = Flip.getState('.item')
// Move first item to end
container.appendChild(container.firstElementChild)
Flip.from(state, {
duration: 1,
absolute: true,
ease: 'power2.inOut'
})
Shuffle Animation
const container = document.querySelector('.list')
const state = Flip.getState('.item')
// Shuffle items
const items = Array.from(container.children)
for (let i = items.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
container.appendChild(items[Math.random() > 0.5 ? j : i])
}
Flip.from(state, {
duration: 0.8,
absolute: true,
stagger: 0.1,
ease: 'power2.out'
})
Drag and Drop Reorder
let state = Flip.getState('.draggable')
// Drag handler
function onDragStart(element) {
state = Flip.getState('.draggable')
}
function onDragEnd(element) {
// Reorder DOM based on drag position
const container = element.parentNode
const afterElement = getDragAfterElement(container, element.clientY)
if (afterElement == null) {
container.appendChild(element)
} else {
container.insertBefore(element, afterElement)
}
Flip.from(state, {
duration: 0.5,
absolute: true,
ease: 'power2.out'
})
}
Responsive Layout
Breakpoint Animation
const mm = gsap.matchMedia()
mm.add('(min-width: 768px)', () => {
const state = Flip.getState('.layout')
document.querySelector('.layout').classList.add('desktop')
Flip.from(state, { duration: 0.8 })
return () => {
const state = Flip.getState('.layout')
document.querySelector('.layout').classList.remove('desktop')
Flip.from(state, { duration: 0.8 })
}
})
Column Change
const container = document.querySelector('.grid')
let state
function changeColumns(cols) {
state = Flip.getState('.item')
container.style.gridTemplateColumns = `repeat(${cols}, 1fr)`
Flip.from(state, {
duration: 1,
absolute: true,
ease: 'power2.out'
})
}
// Change from 3 to 2 columns
changeColumns(2)
Complex Transitions
Accordion
function toggleAccordion(header) {
const content = header.nextElementSibling
const isOpen = content.classList.contains('open')
const state = Flip.getState('.accordion-content')
document.querySelectorAll('.accordion-content').forEach(c => {
c.classList.remove('open')
c.style.height = '0'
})
if (!isOpen) {
content.classList.add('open')
content.style.height = content.scrollHeight + 'px'
}
Flip.from(state, {
duration: 0.5,
ease: 'power2.inOut'
})
}
Tab Switch
function switchTab(tabId) {
const state = Flip.getState('.tab-content')
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active')
tab.style.display = 'none'
})
// Show selected tab
const activeTab = document.querySelector(tabId)
activeTab.classList.add('active')
activeTab.style.display = 'block'
Flip.from(state, {
duration: 0.6,
fade: true,
ease: 'power2.out'
})
}
Modal Animation
function openModal(modal) {
const state = Flip.getState('.modal-content')
modal.classList.add('open')
Flip.from(state, {
duration: 0.5,
scale: true,
ease: 'back.out(1.7)'
})
}
function closeModal(modal) {
const state = Flip.getState('.modal-content')
modal.classList.remove('open')
Flip.from(state, {
duration: 0.3,
scale: true,
ease: 'power2.in',
onComplete: () => {
modal.style.display = 'none'
}
})
}
Enter/Leave Animations
onEnter/onLeave
const state = Flip.getState('.item')
// Add new item
const newItem = document.createElement('div')
newItem.className = 'item'
document.querySelector('.container').appendChild(newItem)
Flip.from(state, {
duration: 0.6,
absolute: true,
onEnter: elements => gsap.from(elements, {
opacity: 0,
scale: 0.8,
duration: 0.5
}),
onLeave: elements => gsap.to(elements, {
opacity: 0,
scale: 0.8,
duration: 0.5
})
})
Enter Only
const state = Flip.getState('.list')
// Add items dynamically
const newItem = document.createElement('div')
newItem.textContent = 'New Item'
document.querySelector('.list').appendChild(newItem)
Flip.from(state, {
duration: 0.5,
absolute: true,
onEnter: elements => {
gsap.from(elements[elements.length - 1], {
opacity: 0,
y: -20,
duration: 0.4
})
}
})
Nested Elements
Parent + Child Flip
// Capture both parent and child
const parentState = Flip.getState('.parent')
const childState = Flip.getState('.child')
// Make changes
document.querySelector('.parent').classList.add('moved')
document.querySelector('.child').classList.add('scaled')
// Animate both
Flip.from([parentState, childState], {
duration: 1,
ease: 'power2.out'
})
Container Flip
const state = Flip.getState('.container')
// Animate container and children
Flip.from(state, {
targets: '.container, .item',
duration: 0.8,
stagger: 0.1,
ease: 'power2.out'
})
ScrollTrigger Integration
Scroll-driven Layout
gsap.registerPlugin(ScrollTrigger, Flip)
gsap.to('.container', {
scrollTrigger: {
trigger: '.container',
start: 'top 80%',
end: 'bottom 20%',
scrub: true,
onUpdate: self => {
const state = Flip.getState('.item')
// Apply layout changes based on scroll progress
const scale = 1 + self.progress * 0.5
document.querySelector('.item').style.transform = `scale(${scale})`
Flip.from(state, { duration: 0.1, scale: true })
}
}
})
Pin + Flip
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.section',
start: 'top top',
end: '+=500',
pin: true,
scrub: true
}
})
tl.add(() => {
const state = Flip.getState('.item')
document.querySelector('.item').classList.add('moved')
Flip.from(state, { duration: 0.5 })
}, 0)
Performance Tips
Use Absolute Wisely
// ❌ Always using absolute
Flip.from(state, { absolute: true })
// ✅ Use only when needed
Flip.from(state, {
absolute: '.moving-items', // Only specific items
duration: 1
})
Batch DOM Changes
// ❌ Multiple Flip animations
const state1 = Flip.getState('.item1')
document.querySelector('.item1').classList.add('moved')
Flip.from(state1, { duration: 0.5 })
const state2 = Flip.getState('.item2')
document.querySelector('.item2').classList.add('moved')
Flip.from(state2, { duration: 0.5 })
// ✅ Batch changes
const state = Flip.getState('.item')
document.querySelector('.item1').classList.add('moved')
document.querySelector('.item2').classList.add('moved')
Flip.from(state, { duration: 0.5, stagger: 0.1 })
Common Mistakes
1. Forgetting Absolute in Flex/Grid
// ❌ Elements jump during animation
const state = Flip.getState('.item')
document.querySelector('.flex-container').classList.add('rearranged')
Flip.from(state, { duration: 1 })
// ✅ Use absolute for flex/grid
Flip.from(state, {
duration: 1,
absolute: true
})
2. Not Capturing State First
// ❌ Changing DOM before capturing state
document.querySelector('.item').classList.add('moved')
const state = Flip.getState('.item')
Flip.from(state, { duration: 1 })
// ✅ Capture state first
const state = Flip.getState('.item')
document.querySelector('.item').classList.add('moved')
Flip.from(state, { duration: 1 })
3. Missing data-flip-id
// ❌ Cross-fade not working
Flip.from(state, { fade: true })
// ✅ Add matching IDs
document.querySelectorAll('.swappable').forEach((item, i) => {
item.setAttribute('data-flip-id', `swap-${i}`)
})
Flip.from(state, { fade: true })
Best Practices
- Capture state before changes - Always capture, then modify DOM
- Use absolute in flex/grid - Prevents layout shifts during animation
- Add data-flip-id for fades - Enable cross-fade for swapped elements
- Batch DOM changes - One Flip animation for multiple changes
- Consider scale vs dimensions - Scale is more performant for size changes
- Use onEnter/onLeave - Animate elements entering/leaving DOM
- Combine with ScrollTrigger - Create scroll-driven layout effects
- Test on mobile - Layout animations can behave differently
Quick Reference
| Feature | Method |
|---|---|
| Get state | Flip.getState(targets) |
| Animate flip | Flip.from(state, { duration }) |
| Absolute positioning | absolute: true |
| Cross-fade | fade: true |
| Scale animation | scale: true |
| Enter animation | onEnter: elements => ... |
| Leave animation | onLeave: elements => ... |
| Absolute on leave | absoluteOnLeave: true |
Weekly Installs
1
Repository
microck/gsap-skillsFirst Seen
6 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
warp1