The most effective methods to study is by recreating an interplay you’ve seen out within the wild and constructing it from scratch. It pushes you to note the small particulars, perceive the logic behind the animation, and strengthen your problem-solving expertise alongside the way in which.
So as we speak we’ll dive into rebuilding the graceful, draggable product grid from the Palmer web site, initially crafted by Unusual with Kevin Masselink, Alexis Sejourné, and Dylan Brouwer. The objective is to know how this sort of interplay works below the hood and code the fundamentals from scratch.
Alongside the way in which, you’ll learn to construction a versatile grid, implement draggable navigation, and add clean scroll-based motion. We’ll additionally discover the best way to animate merchandise as they enter or go away the viewport, and end with a cultured product element transition utilizing Flip and SplitText for dynamic textual content reveals.
Let’s get began!
Grid Setup
The Markup
Let’s not attempt to be unique and, as at all times, begin with the fundamentals. Earlier than we get into the animations, we want a transparent construction to work with — one thing easy, predictable, and straightforward to construct upon.
<div class="container">
<div class="grid">
<div class="column">
<div class="product">
<div><img src="./public/img-3.png" /></div>
</div>
<div class="product">
<div><img src="./public/img-7.png" /></div>
</div>
<!-- repeat -->
</div>
<!-- repeat -->
</div>
</div>
What we’ve here’s a .container
that fills the viewport, inside which sits a .grid
divided into vertical columns. Every column stacks a number of .product
parts, and each product wraps round a picture. It’s a minimal setup, but it surely lays the inspiration for the draggable, animated expertise we’re about to create.
The Fashion
Now that we’ve obtained the construction, let’s add some styling to make the grid usable. We’ll maintain issues easy and use Flexbox as a substitute of CSS Grid, since Flexbox makes it simpler to deal with vertical offsets for alternating columns. This strategy retains the structure versatile and prepared for animation.
.container {
place: mounted;
width: 100vw;
peak: 100vh;
prime: 0;
left: 0;
}
.grid {
place: absolute;
show: flex;
hole: 5vw;
cursor: seize;
}
.column {
show: flex;
flex-direction: column;
hole: 5vw;
}
.column:nth-child(even) {
margin-top: 10vw;
}
.product {
place: relative;
width: 18.5vw;
aspect-ratio: 1 / 1;
div {
width: 18.5vw;
aspect-ratio: 1 / 1;
}
img {
place: absolute;
width: 100%;
peak: 100%;
object-fit: comprise;
}
}

Animation
Okay, setup’s out of the way in which — now let’s leap into the enjoyable half.
When growing interactive experiences, it helps to interrupt issues down into smaller components. That approach, each bit may be dealt with step-by-step with out feeling overwhelming.
Right here’s the construction I adopted for this challenge:
1 – Introduction / Preloader
2 – Grid Navigation
3 – Product’s element view transition
Introduction / Preloader
First, the grid isn’t centered by default, so we’ll repair that with a small utility perform. This makes certain the grid at all times sits neatly in the course of the display, irrespective of the viewport dimension.
centerGrid() {
const gridWidth = this.grid.offsetWidth
const gridHeight = this.grid.offsetHeight
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
const centerX = (windowWidth - gridWidth) / 2
const centerY = (windowHeight - gridHeight) / 2
gsap.set(this.grid, {
x: centerX,
y: centerY
})
}
Within the unique Palmer reference, the expertise begins with merchandise showing one after the other in a barely random order. After that reveal, the entire grid easily zooms into place.
To maintain issues easy, we’ll begin with each the container and the merchandise scaled all the way down to 0.5
and the merchandise absolutely clear. Then we animate them again to full dimension and opacity, including a random stagger so the pictures pop in at barely completely different occasions.
The result’s a dynamic however light-weight introduction that units the tone for the remainder of the interplay.
intro() {
this.centerGrid()
const timeline = gsap.timeline()
timeline.set(this.dom, { scale: .5 })
timeline.set(this.merchandise, {
scale: 0.5,
opacity: 0,
})
timeline.to(this.merchandise, {
scale: 1,
opacity: 1,
length: 0.6,
ease: "power3.out",
stagger: { quantity: 1.2, from: "random" }
})
timeline.to(this.dom, {
scale: 1,
length: 1.2,
ease: "power3.inOut"
})
}
Grid Navigation
The grid seems to be good. Subsequent, we want a strategy to navigate it: GSAP’s Draggable plugin is simply what we want.
setupDraggable() {
this.draggable = Draggable.create(this.grid, {
kind: "x,y",
bounds: {
minX: -(this.grid.offsetWidth - window.innerWidth) - 200,
maxX: 200,
minY: -(this.grid.offsetHeight - window.innerHeight) - 100,
maxY: 100
},
inertia: true,
allowEventDefault: true,
edgeResistance: 0.9,
})[0]
}
It might be nice if we may add scrolling too.
window.addEventListener("wheel", (e) => {
e.preventDefault()
const deltaX = -e.deltaX * 7
const deltaY = -e.deltaY * 7
const currentX = gsap.getProperty(this.grid, "x")
const currentY = gsap.getProperty(this.grid, "y")
const newX = currentX + deltaX
const newY = currentY + deltaY
const bounds = this.draggable.vars.bounds
const clampedX = Math.max(bounds.minX, Math.min(bounds.maxX, newX))
const clampedY = Math.max(bounds.minY, Math.min(bounds.maxY, newY))
gsap.to(this.grid, {
x: clampedX,
y: clampedY,
length: 0.3,
ease: "power3.out"
})
}, { passive: false })
We are able to additionally make the merchandise seem as we transfer across the grid.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.goal === this.currentProduct) return
if (entry.isIntersecting) {
gsap.to(entry.goal, {
scale: 1,
opacity: 1,
length: 0.5,
ease: "power2.out"
})
} else {
gsap.to(entry.goal, {
opacity: 0,
scale: 0.5,
length: 0.5,
ease: "power2.in"
})
}
})
}, { root: null, threshold: 0.1 })
Product’s element view transition
While you click on on a product, an overlay opens and shows the product’s particulars.
Throughout this transition, the product’s picture animates easily from its place within the grid to its place contained in the overlay.
We construct a easy overlay with minimal construction and styling and add an empty <div>
that may comprise the product picture.
<div class="particulars">
<div class="details__title">
<p>The title</p>
</div>
<div class="details__body">
<div class="details__thumb"></div>
<div class="details__texts">
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit...</p>
</div>
</div>
</div>
.particulars {
place: absolute;
prime: 0;
left: 0;
width: 50vw;
peak: 100vh;
padding: 4vw 2vw;
background-color: #FFF;
remodel: translate3d(50vw, 0, 0);
}
.details__thumb {
place: relative;
width: 25vw;
aspect-ratio: 1 / 1;
z-index: 3;
will-change: remodel;
}
/* and so on */
To attain this impact, we use GSAP’s Flip plugin. This plugin makes it straightforward to animate parts between two states by calculating the variations in place, dimension, scale, and different properties, then animating them seamlessly.
We seize the state of the product picture, transfer it into the main points thumbnail container, after which animate the transition from the captured state to its new place and dimension.
showDetails(product) {
gsap.to(this.dom, {
x: "50vw",
length: 1.2,
ease: "power3.inOut",
})
gsap.to(this.particulars, {
x: 0,
length: 1.2,
ease: "power3.inOut",
})
this.flipProduct(product)
}
flipProduct(product) {
this.currentProduct = product
this.originalParent = product.parentNode
if (this.observer) {
this.observer.unobserve(product)
}
const state = Flip.getState(product)
this.detailsThumb.appendChild(product)
Flip.from(state, {
absolute: true,
length: 1.2,
ease: "power3.inOut",
});
}
We are able to add completely different text-reveal animations when a product’s particulars are proven, utilizing the SplitText plugin.
const splitTitles = new SplitText(this.titles, {
kind: "traces, chars",
masks: "traces",
charsClass: "char"
})
const splitTexts = new SplitText(this.texts, {
kind: "traces",
masks: "traces",
linesClass: "line"
})
gsap.to(splitTitles.chars, {
y: 0,
length: 1.1,
delay: 0.4,
ease: "power3.inOut",
stagger: 0.025
});
gsap.to(splitTexts.traces, {
y: 0,
length: 1.1,
delay: 0.4,
ease: "power3.inOut",
stagger: 0.05
});
Closing ideas
I hope you loved following alongside and picked up some helpful strategies. After all, there’s at all times room for additional refinement—like experimenting with completely different easing capabilities or timing—however the core concepts are all right here.
With this strategy, you now have a useful toolkit for constructing clean, draggable product grids and even easy picture galleries. It’s one thing you may adapt and reuse in your personal initiatives, and a superb reminder of how a lot may be achieved with GSAP and its plugins when used thoughtfully.
An enormous because of Codrops and to Manoela for giving me the chance to share this primary article right here 🙏 I’m actually wanting ahead to listening to your suggestions and ideas!
See you round 👋