Palmer’s Draggable Product Grid Relve with GSAP | Codrops

Palmer’s Draggable Product Grid Relve with GSAP | Codrops

One of the best ways to learn is to recreate an interaction that you have seen in the wild and rebuilding them completely. It pushes you to notice the small details, to understand the logic behind the animation and to strengthen your problem -solving skills along the way.

So today we will dive into the rebuilding of the flexible, draggable product grant of the Palmer website, originally made by Unusual of Kevin Masselink” Alexis SejournéAnd Dylan Brouwer. The goal is to understand how this type of interaction works under the hood and to completely cod the base.

Along the way you will learn how to structure a flexible grid, implement draggable navigation and add a smoothly on scroll -based movement. We will also investigate how they can animate products when entering or leaving the viewport, and ending with a polished product detail transition using Flip and Splitext for dynamic text.

Let’s start!

Grid setup

The Markup

Let’s not try to be original and, as always, start with the basics. Before we enter into the animations, we need a clear structure to work with – something simple, predictable and easy to build.

What we have here is one .container That fills the viewport, of which one .grid Divided into vertical columns. Each column stacks several .product Elements, and each product wraps around an image. It is a minimal arrangement, but it lays the foundation for the draggable, animated experience that we are going to create.

Style

Now that we have the structure, let’s add some styling to make the grid usable. We keep things simple and use Flexbox instead of CSS -Raster, because Flexbox makes it easier to process vertical offsets for varied columns. This approach keeps the layout flexible and ready for animation.

.container {
  position: fixed;
  width: 100vw;
  height: 100vh;
  top: 0;
  left: 0;
}

.grid {
  position: absolute;
  display: flex;
  gap: 5vw;
  cursor: grab;
}

.column {
  display: flex;
  flex-direction: column;
  gap: 5vw;
}

.column:nth-child(even) {
  margin-top: 10vw;  
}

.product {
  position: relative;
  width: 18.5vw;
  aspect-ratio: 1 / 1;

  div {
    width: 18.5vw;
    aspect-ratio: 1 / 1;
  }

  img {
    position: absolute;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
}

Animation

Okay, the setup is out of the way – let’s jump in the nice part.

In developing interactive experiences it helps to split things into smaller parts. In this way, every piece can be treated step by step without feeling overwhelming.

Here is the structure that I followed for this project:

1 – Introduction / Preloader
2 – Grid -Navigation
3 – Detail transition of the product

Introduction / Preloader

Firstly, the grid is not centered as standard, so we repair it with a small utility. This ensures that the grid is always neatly in the middle of the screen, regardless of the view port size.

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
  })
}

In the original Palmer reference, experience begins with products that appear one by one in a somewhat random order. After that unveiling, the entire grid zooms smoothly in place.

To keep it easy, we start with both the container and the products that have been reduced 0.5 And the products completely transparent. We then animal back to full size and coverage, so that a random Wankelaar is added, so that the images enter slightly different times.

The result is a dynamic but lightweight introduction that sets the tone for the rest of the interaction.

intro() {
  this.centerGrid()

  const timeline = gsap.timeline()

  timeline.set(this.dom, { scale: .5 })
  timeline.set(this.products, {
    scale: 0.5,
    opacity: 0,
  })

  timeline.to(this.products, {
    scale: 1,
    opacity: 1,
    duration: 0.6,
    ease: "power3.out",
    stagger: { amount: 1.2, from: "random" }
  })
  timeline.to(this.dom, {
    scale: 1,
    duration: 1.2,
    ease: "power3.inOut"
  })
}

Raster navigation

The schedule looks good. Then we need a way to navigate with it: GSAPs Bearing Plug -in is exactly what we need.

setupDraggable() {
  this.draggable = Draggable.create(this.grid, {
    type: "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 would be great if we could also add scrolling.

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,
    duration: 0.3,
    ease: "power3.out"
  })
}, { passive: false })

We can also let the products appear when we move through the grid.

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.target === this.currentProduct) return
    if (entry.isIntersecting) {
      gsap.to(entry.target, {
        scale: 1,
        opacity: 1,
        duration: 0.5,
        ease: "power2.out"
      })
    } else {
      gsap.to(entry.target, {
        opacity: 0,
        scale: 0.5,
        duration: 0.5,
        ease: "power2.in"
      })
    }
  })
}, { root: null, threshold: 0.1 })

Product Detail View Transition

When you click on a product, an overlay is opened and the details of the product display.
During this transition, the image of the product is flexibly animated from its position in the grid to his position in the Overlay.

We build a simple overlay with minimal structure and styling and add an empty

that will contain the product image.

Lorem ipsum dolor, sit amet consectetur adipisicing elit...

.details {
  position: absolute;
  top: 0;
  left: 0;
  width: 50vw;
  height: 100vh;
  padding: 4vw 2vw;
  background-color: #FFF;

  transform: translate3d(50vw, 0, 0);
}

.details__thumb {
  position: relative;
  width: 25vw;
  aspect-ratio: 1 / 1;
  z-index: 3;
  will-change: transform;
}

/* etc */

To achieve this effect, we use GSAPs Flip -Plug -On. This plug -in makes it easy to animate elements between two states by calculating the differences in position, size, scale and other properties and then seamlessly animate them.

We catch the status of the product image, move it to the details under the miniature container and then animate the transition from the set state to its new position and size.

showDetails(product) {
  gsap.to(this.dom, {
    x: "50vw",
    duration: 1.2,
    ease: "power3.inOut",
  })

  gsap.to(this.details, {
    x: 0,
    duration: 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,
    duration: 1.2,
    ease: "power3.inOut",
  });
}

We can add different Text-Reveal animations when the details of a product are shown, using the Split text plug -in.

const splitTitles = new SplitText(this.titles, {
  type: "lines, chars",
  mask: "lines",
  charsClass: "char"
})

const splitTexts = new SplitText(this.texts, {
  type: "lines",
  mask: "lines",
  linesClass: "line"
})

gsap.to(splitTitles.chars, {
  y: 0,
  duration: 1.1,
  delay: 0.4,
  ease: "power3.inOut",
  stagger: 0.025
});

gsap.to(splitTexts.lines, {
  y: 0,
  duration: 1.1,
  delay: 0.4,
  ease: "power3.inOut",
  stagger: 0.05
});

Last thoughts

I hope you enjoyed following and picked up some useful techniques. Of course there is always room for further refinement - such as experimenting with different relaxation functions or timing - but the core ideas are all there.

With this approach you now have a handy toolkit for building flexible, draggable product graters or even simple image galleries. It is something that you can adjust and reuse in your own projects, and a good memory of how much can be achieved with GSAP and the plug -in when they are used carefully.

A huge thanks to Codrops and to Manoela for giving the opportunity to share this first article here 🙏 I am really looking forward to hearing your feedback and thoughts!

Goodbye 👋

Draggable Grid GSAP

#Palmers #Draggable #Product #Grid #Relve #GSAP #Codrops

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *