My (design) accomplice, Gaetan Ferhah, likes to ship me his design and movement experiments all through the week. It’s at all times enjoyable to see what he’s engaged on, and it usually sparks concepts for my very own tasks. Someday, he despatched over a fast idea for making a product grid really feel a bit extra inventive and interactive. 💬 The concept for this tutorial got here from that message.
We’ll discover a “grid to preview” hover interplay that transforms product playing cards right into a full preview. As with many animations and interactions, there are often a number of methods to strategy the implementation—ranging in complexity. It might probably really feel intimidating (or nearly unattainable) to recreate a designer’s imaginative and prescient from scratch. However I’m an enormous fan of simplifying wherever doable and leaning on optical illusions (✨ pretend it ’til you make it ✨).
For this tutorial, I knew I wished to maintain issues simple and recreate the impact of puzzle items shifting into place utilizing a mixture of clip-path
animation and a picture overlay.
Let’s break it down in a couple of steps:
- Structure and Overlay (HTML, CSS)Arrange the preliminary structure and punctiliously match the place of the preview overlay to the grid.
- Construct JavaScript construction (JavaScript)Creating some lessons to maintain us organised, add some interactivity (occasion listeners).
- Clip-Path Creation and Animation (CSS, JS, GSAP)Including and animating the
clip-path
, together with some calculations on resize—this types a key a part of the puzzle impact. - Transferring Product Playing cards (JS, GSAP)Arrange animations to maneuver the product playing cards in direction of one another on hover.
- Preview Picture Scaling (JS, GSAP)Barely cutting down the preview overlay in response to the inward motion of the opposite components.
- Including Pictures (HTML, JS, GSAP)Sufficient with the strong colors, let’s add some photographs and a gallery animation.
- Debouncing occasions (JS)Debouncing the mouse-enter occasion to forestall extreme triggering and scale back jitter.
- Ultimate tweaks Crossed the t’s and dotted the i’s—small clean-ups and enhancements.
Structure and Overlay
On the basis of each good tutorial is a strong HTML construction. On this step, we’ll create two key components: the product grid and the overlay for the preview playing cards. Since each want an analogous structure, we’ll place them inside the identical container (.merchandise
).
Our grid will include 8 merchandise (4 columns by 2 rows) with a gutter of 5vw
. To maintain issues easy, I’m solely including the corresponding li
components for the merchandise, however not but including some other components. Within the HTML, you’ll discover there are two preview containers: one for the left facet and one for the appropriate. If you wish to see the preview overlays immediately, head to the CodePen and set the opacity of .product-preview
to 1
.
Why I Opted for Two Containers
At first, I deliberate to make use of only one preview container and transfer it to the other facet of the hovered card by updating the grid-column-start
. That strategy labored wonderful—till I obtained to testing.
Once I hovered over a product card on the left and rapidly switched to 1 on the appropriate, I realised the issue: with just one container, I additionally had only one timeline controlling the whole lot inside it. That made it mainly unattainable to handle the “in/out” transition between sides easily.
So, I made a decision to go together with two containers—one for the left facet and one for the appropriate. This manner, I may animate each side independently and keep away from timeline conflicts when switching between them.
See the Pen
Untitled by Gwen Bogaert (@gwen-bo)
on CodePen.
JavaScript Set-up
On this step, we’ll add some lessons to maintain issues structured earlier than including our occasion listeners and initiating our timelines. To maintain issues organised, let’s cut up it into two lessons: ProductGrid
and ProductPreview
.
ProductGrid
can be pretty primary, answerable for dealing with the cut up between left and proper, and managing top-level occasion listeners (resembling mouseenter
and mouseleave
on the product playing cards, and a common resize).
ProductPreview
is the place the magic occurs. ✨ That is the place we’ll management the whole lot that occurs as soon as a mouse occasion is triggered (enter or depart). To cross the ‘lively’ product, we’ll outline a setProduct
technique, which, in later steps, will act as the place to begin for controlling our GSAP animation(s).
Splitting Merchandise (Left – Proper)
Within the ProductGrid
class, we are going to cut up all of the merchandise into left and proper teams. We now have 8 merchandise organized in 4 columns, with every row containing 4 gadgets. We’re splitting the product playing cards into left and proper teams primarily based on their column place.
this.ui.merchandise.filter((_, i) => i % 4 === 2 || i % 4 === 3)
The logic depends on the modulo or the rest operator. The road above teams the product playing cards on the appropriate. We use the index (i
) to test if it’s within the third (i % 4 === 2
) or 4th (i % 4 === 3
) place of the row (bear in mind, indexing begins at 0). The remaining merchandise (with i % 4 === 0
or i % 4 === 1
) can be grouped on the left.
Now that we all know which merchandise belong to the left and proper sides, we are going to provoke a ProductPreview
for each side and cross alongside the merchandise array. This may enable us to outline productPreviewRight
and productPreviewLeft
.
To finalize this step, we are going to outline occasion listeners. For every product, we’ll pay attention for mouseenter
and mouseleave
occasions, and both set or unset the lively product (each internally and within the corresponding ProductPreview
class). Moreover, we’ll add a resize
occasion listener, which is at present unused however can be arrange for future use.
That is the place we’re at thus far (solely adjustments in JavaScript):
See the Pen
Tutorial – step 2 (JavaScript construction) by Gwen Bogaert (@gwen-bo)
on CodePen.
Clip-path
On the base of our impact lies the clip-path
property and the power to animate it with GSAP. Should you’re not conversant in utilizing clip-path
to clip content material, I extremely suggest this text by Sarah Soueidan.
Regardless that I’ve used clip-path
in lots of my tasks, I usually battle to recollect precisely outline the form I’m on the lookout for. As earlier than, I’ve as soon as once more turned to the fantastic software Clippy, to get a head begin on defining (or exploring) clip-path
shapes. For me, it helps demystify which worth influences which a part of the form.
Let’s begin with the cross (from Clippy) and modify the factors to create a extra mathematical-looking cross (✚) as an alternative of the non secular model (✟).
clip-path: polygon(10% 25%, 35% 25%, 35% 0%, 65% 0%, 65% 25%, 90% 25%, 90% 50%, 65% 50%, 65% 100%, 35% 100%, 35% 50%, 10% 50%);
Be at liberty to experiment with among the values, and shortly you’ll discover that with small changes, we are able to get a lot nearer to the specified form! For instance, by stretching the horizontal arms fully to the perimeters (set to 10%
and 90%
earlier than) and shifting the whole lot extra equally in direction of the middle (with a ten% distinction from the middle — so both 40%
or 60%
).
clip-path: polygon(0% 40%, 40% 40%, 40% 0%, 60% 0%, 60% 40%, 100% 40%, 100% 60%, 60% 60%, 60% 100%, 40% 100%, 40% 60%, 0% 60%);
And bada bing, bada increase! This clip-path
nearly instantly creates the phantasm that our single preview container is cut up into 4 elements — precisely the impact we wish to obtain! Now, let’s transfer on to animating the clip-path
to get one step nearer to our closing outcome:
Animating Clip-paths
The idea of animating clip-paths
is comparatively easy, however there are a couple of key issues to remember to make sure a clean transition. One essential consideration is that it’s finest to outline an equal variety of factors for each the beginning and finish shapes.
The concept is pretty simple: we start with the clipped elements hidden, and by the tip of the animation, we wish the clip-path
to vanish, revealing your complete preview container (by making the arms of the cross so skinny that they’re barely seen or not seen in any respect). This may be achieved simply with a fromTo
animation in GSAP (although it’s additionally supported in CSS animations).

The Catch
You may suppose, “That’s it, we’re achieved!” — however alas, there’s a catch relating to utilizing this as our puzzle impact. To make it look practical, we have to be sure that the form of the cross aligns with the underlying product grid. And that’s the place a little bit of JavaScript is available in!
We have to issue within the gutter of our grid (5vw
) to calculate the width of the arms of our cross form. It may’ve been so simple as including or subtracting (half!) of the gutter to/from the 50%, however… there’s a catch within the catch!

We’re not working with a sq., however with a rectangle. Since our values are percentages, subtracting 2.5vw
(half of the gutter) from the middle wouldn’t give us equal-sized arms. It is because there would nonetheless be a distinction between the x and y dimensions, even when utilizing the identical proportion worth. So, let’s check out repair that:
onResize() {
const { width, top } = this.container.getBoundingClientRect()
const vw = window.innerWidth / 100
const armWidthVw = 5
const armWidthPx = armWidthVw * vw
this.armWidth = {
x: (armWidthPx / width) * 100,
y: (armWidthPx / top) * 100
}
}
Within the code above (triggered on every resize), we get the width and top of the preview container (which spans 4 product playing cards — 2 columns and a pair of rows). We then calculate what proportion 5vw
could be, relative to each the width and top.
To conclude this step, we’d have one thing like:
See the Pen
Tutorial – step 3 (clip path) by Gwen Bogaert (@gwen-bo)
on CodePen.
Transferring Product Playing cards
One other step within the puzzle impact is shifting the seen product playing cards collectively so they seem to kind one piece. This step is pretty easy — we already know the way a lot they should transfer (once more, gutter divided by 2 = 2.5vw
). The one factor we have to work out is whether or not a card wants to maneuver up, down, left, or proper. And that’s the place GSAP involves the rescue!
We have to outline each the vertical (y) and horizontal (x) motion for every aspect primarily based on its index within the listing. Since we solely have 4 gadgets, and they should transfer inward, we are able to test whether or not the index is odd and even to find out the specified worth for the horizontal motion. For vertical motion, we are able to resolve whether or not it ought to transfer to the highest or backside relying on the place (prime or backside).
In GSAP, many properties (like x
, y
, scale
, and so on.) can settle for a perform as an alternative of a set worth. Once you cross a perform, GSAP calls it for every goal aspect individually.
Horizontal (x): playing cards with a fair index (0, 2
) get shifted proper by 2.5vw
, the opposite (two) transfer to the left. Vertical (y): playing cards with an index decrease than 2 (0,1
) are situated on the prime, so want to maneuver down, the opposite (two) transfer up.
{
x: (i) => {
return i % 2 === 0 ? '2.5vw' : '-2.5vw'
},
y: (i) => {
return i < 2 ? '2.5vw' : '-2.5vw'
}
}
See the Pen
Tutorial – step 3 (clip path) by Gwen Bogaert (@gwen-bo)
on CodePen.
Preview Picture (Scaling)
Cool, we’re slowly getting there! We now have our clip-path
animating out and in on hover, and the playing cards are shifting inward as nicely. Nonetheless, you may discover that the playing cards and the picture not have a precise overlap as soon as the playing cards have been moved. To repair that and make the whole lot extra seamless, we’ll apply a slight scale to the preview container.

That is the place a bit of additional calculation is available in, as a result of we wish it to scale relative to the gutter. So we take into consideration the peak and width of the container.
onResize() {
const { width, top } = this.container.getBoundingClientRect()
const vw = window.innerWidth / 100
// ...armWidth calculation (see earlier step)
const widthInVw = width / vw
const heightInVw = top / vw
const shrinkVw = 5
this.scaleFactor = {
x: (widthInVw - shrinkVw) / widthInVw,
y: (heightInVw - shrinkVw) / heightInVw
}
}
This calculation determines a scale issue to shrink our preview container inward, matching the playing cards coming collectively. First, the rectangle’s width/top (in pixels) is transformed into viewport width models (vw) by dividing it by the pixel worth of 1vw
. Subsequent, the shrink quantity (5vw
) is subtracted from that width/top. Lastly, the result’s divided by the unique width in vw to calculate the dimensions issue (which can be barely under 1). Since we’re working with a rectangle, the dimensions issue for the x and y axes can be barely completely different.
Within the codepen under, you’ll see the puzzle impact coming alongside properly on every container. Pink are the product playing cards (not shifting), purple and blue are the preview containers.
See the Pen
Tutorial – step 4 (shifting playing cards) by Gwen Bogaert (@gwen-bo)
on CodePen.
Including Footage
Let’s make our grid slightly extra enjoyable to have a look at!
On this step, we’re going so as to add the product photographs to our grid, and the product preview photographs contained in the preview container. As soon as that’s achieved, we’ll begin our picture gallery on hover.
The HTML adjustments are comparatively easy. We’ll add a picture to every product li
aspect and… not do something with it. We’ll simply depart the picture as is.
The remainder of the magic will occur contained in the preview container. Every container will maintain the preview photographs of the merchandise from the opposite facet (people who can be seen). So, the left container will include the pictures of the 4 merchandise on the appropriate, and the appropriate container will include the pictures of the 4 merchandise on the left. Right here’s an instance of one among these:
As soon as that’s achieved, we are able to initialise by querying these photographs within the constructor of the ProductPreview
, sorting them by their dataset.id
. This may enable us to simply entry the pictures later through the data-index
attribute that every product has. To sum up, on the finish of our animate-in timeline, we are able to name startPreviewGallery
, which is able to deal with our gallery impact.
startPreviewGallery(id) {
const photographs = this.ui.previewImagesPerID[id]
const timeline = gsap.timeline({ repeat: -1 })
// first picture is already seen (don't cover)
gsap.set([...images].slice(1), { opacity: 0 })
photographs.forEach((picture) => {
timeline
.set(photographs, { opacity: 0 }) // Cover all photographs
.set(picture, { opacity: 1 }) // Present solely this one
.to(picture, { period: 0, opacity: 1 }, '+=0.5')
})
this.galleryTimeline = timeline
}
Debouncing
One factor I’d love to do is debounce hover results, particularly if they're extra advanced or take longer to finish. To realize this, we’ll use a easy (and vanilla) JavaScript strategy with setTimeout
. Every time a hover occasion is triggered, we’ll set a really quick timer that acts as a debouncer, stopping the impact from firing if somebody is simply “passing by” on their strategy to the product card on the opposite facet of the grid.
I ended up utilizing a 100ms “cooldown” earlier than triggering the animation, which helped scale back pointless animation begins and minimise jitter when interacting with the playing cards.
productMouseEnter(product, preview) {
// If one other timer (aka hover) was operating, cancel it
if (this.hoverDelay) {
clearTimeout(this.hoverDelay)
this.hoverDelay = null
}
// Begin a brand new timer
this.hoverDelay = setTimeout(() => {
this.activeProduct = product
preview.setProduct(product)
this.hoverDelay = null // clear reference
}, 100)
}
productMouseLeave() {
// If person leaves earlier than debounce completes
if (this.hoverDelay) {
clearTimeout(this.hoverDelay)
this.hoverDelay = null
}
if (this.activeProduct) {
const preview = this.getProductSide(this.activeProduct)
preview.setProduct(null)
this.activeProduct = null
}
}
Ultimate Tweaks
I can’t consider we’re nearly there! Subsequent up, it’s time to piece the whole lot collectively and add some small tweaks, like experimenting with easings, and so on. The ultimate timeline I ended up with (which performs or reverses relying on mouseenter
or mouseleave
) is:
buildTimeline() {
const { x, y } = this.armWidth
this.timeline = gsap
.timeline({
paused: true,
defaults: {
ease: 'power2.inOut'
}
})
.addLabel('preview', 0)
.addLabel('merchandise', 0)
.fromTo(this.container, { opacity: 0 }, { opacity: 1 }, 'preview')
.fromTo(this.container, { scale: 1 }, { scaleX: this.scaleFactor.x, scaleY: this.scaleFactor.y, transformOrigin: 'middle middle' }, 'preview')
.to(
this.merchandise,
{
opacity: 0,
x: (i) => {
return i % 2 === 0 ? '2.5vw' : '-2.5vw'
},
y: (i) => {
return i < 2 ? '2.5vw' : '-2.5vw'
}
},
'merchandise'
)
.fromTo(
this.masked,
{
clipPath: `polygon(
${50 - x / 2}% 0%,
${50 + x / 2}% 0%,
${50 + x / 2}% ${50 - y / 2}%,
100% ${50 - y / 2}%,
100% ${50 + y / 2}%,
${50 + x / 2}% ${50 + y / 2}%,
${50 + x / 2}% 100%,
${50 - x / 2}% 100%,
${50 - x / 2}% ${50 + y / 2}%,
0% ${50 + y / 2}%,
0% ${50 - y / 2}%,
${50 - x / 2}% ${50 - y / 2}%
)`
},
{
clipPath: `polygon(
50% 0%,
50% 0%,
50% 50%,
100% 50%,
100% 50%,
50% 50%,
50% 100%,
50% 100%,
50% 50%,
0% 50%,
0% 50%,
50% 50%
)`
},
'preview'
)
}
Ultimate End result
📝 A fast word on usability & accessibility
Whereas this interplay could look cool and visually partaking, it’s essential to be aware of usability and accessibility. In its present kind, this impact depends fairly closely on movement and hover interactions, which might not be excellent for all customers. Right here are some things that must be thought-about for those who’d be planning on implementing an analogous impact:
- Movement sensitivity: Make sure you respect the person’s
prefers-reduced-motion
setting. You'll be able to simply test this with a media question and supply a simplified or static various for customers preferring minimal movement. - Keyboard navigation: Since this interplay is hover-based, it’s not at present accessible through keyboard. Should you’d prefer to make it extra inclusive, take into account including assist for focus occasions and guaranteeing that each one interactive components could be reached and triggered utilizing a keyboard.
Consider this as a playful, exploratory layer — not a basis. Use it thoughtfully, and prioritise accessibility the place it counts. 💛
Acknowledgements
I'm conscious that this tutorial assumes a super situation of solely 8 merchandise, as a result of what occurs if in case you have extra? I didn’t try it out myself, however the essential half is that the preview containers really feel like a precise overlay of the product grid. If extra playing cards are current, you would attempt ‘mapping’ the coordinates of the preview container to the 8 merchandise which might be fully in view. Or.. go loopy with your personal strategy for those who had one other thought. That’s the great thing about it, there’s at all times many approaches that may result in the identical (visible) end result. 🪄
Thanks a lot for following alongside! An enormous due to Codrops for giving me the chance to contribute. I’m excited to see what you’ll create when impressed by this tutorial! If in case you have any questions, be happy to drop me a line!