Interactive Masonry Image Gallery with Smooth Animations

in tutorials by Mirza Hodzic8 min read
Interactive Masonry Image Gallery with Smooth Animations

Are you looking to add an eye-catching and interactive image gallery to your website? Look no further! In this blog post, we'll walk you through the process of creating an impressive masonry image gallery with smooth animations using some powerful web development tools and libraries.

The interactive masonry image gallery we'll create combines the dynamic grid layout provided by the Masonry library with smooth animations powered by GSAP (GreenSock Animation Platform) and interactive scrolling effects using ScrollMagic. This combination allows us to create a visually stunning and interactive gallery that engages users and provides a seamless browsing experience.

Interactive Masonry Image Gallery with Smooth Animations - Sketch

Let's dive into the code with scuba gear on and see how it all comes together! :)

HTML Structure

Let's start by examining the HTML structure of our gallery. The gallery is wrapped in a section element with the class grid-holder. Inside this section, we have a div element with the classes grid and masonry, which will serve as the container for our masonry layout.

1<section class="grid-holder">
2  <div class="grid masonry">
3    <!-- "...masonry items..." -->
4  </div>

Within the grid, we have individual items represented by div elements with the class masonry__item. Each item consists of a figure element containing a caption, content, background image, and additional details. The details include an icon and two circles with a text. The structure for each item is repeated for multiple images in the gallery.

You can customize the content and styling of each item to suit your needs.

1<!-- ... -->
3<!-- wannabedev:demo:grid:item (small item) START -->
4<div class="masonry__item small">
5  <figure>
6    <figcaption>#1</figcaption>
7    <div class="content">
8      <h2>Rainy Window</h2>
9    </div>
10    <div class="background" style="background-image: url(assets/img/img-1.jpeg);"></div>
11    <div class="detail">
12      <span class="icon"></span>
13      <span class="circles">
14        <div class="circle circle1"><span>FREE</span></div>
15        <div class="circle circle2"><span></span></div>
16      </span>
17    </div>
18  </figure>
20<!-- wannabedev:demo:grid:item (small item) END -->
22<!-- wannabedev:demo:grid:item (middle item) START -->
23<div class="masonry__item middle">
24  <figure>
25    <figcaption>#2</figcaption>
26    <div class="content">
27      <h2>Rainy Window</h2>
28    </div>
29    <div class="background" style="background-image: url(assets/img/img-2.jpeg);"></div>
30    <div class="detail">
31      <span class="icon"></span>
32      <span class="circles">
33        <div class="circle circle1"><span>€9.99</span></div>
34        <div class="circle circle2"><span></span></div>
35      </span>
36    </div>
37  </figure>
39<!-- wannabedev:demo:grid:item (middle item) END -->
41<!-- wannabedev:demo:grid:item (big item) START -->
42<div class="masonry__item big">
43  <figure>
44    <figcaption>#3</figcaption>
45    <div class="content">
46      <h2>Rainy Window</h2>
47    </div>
48    <div class="background" style="background-image: url(assets/img/img-3.jpeg);"></div>
49    <div class="detail">
50      <span class="icon"></span>
51      <span class="circles">
52        <div class="circle circle1"><span>FREE</span></div>
53        <div class="circle circle2"><span></span></div>
54      </span>
55    </div>
56  </figure>
58<!-- wannabedev:demo:grid:item (big item) END -->
60<!-- ... -->

JavaScript Functionality

It is time to add some interactions using JavaScript. The JavaScript code is responsible for bringing our image gallery to life. Let's break it down and understand its functionality.

Masonry Grid

The Masonry library offers several key features and benefits. Firstly, it automatically adjusts the positioning of elements based on the available space, allowing for a visually pleasing and optimized layout across different screen sizes and orientations. This responsiveness is achieved through the use of CSS transitions and transforms.

To use the Masonry library, you need to include the library file in your project and create an instance of the Masonry class. The constructor of the Masonry class accepts two parameters: the container element (referred to as the grid in the code) and a configuration object with options to customize the behavior of the layout. One important option is the itemSelector, which specifies the CSS selector for individual grid items within the container.

The initWannabeGallery function is our entry point. It starts by selecting the grid container using document.querySelector('.grid'). Then, it creates a new instance of the Masonry library with the grid container and specifies the item selector using CSS class masonry__item.

1const initWannabeGallery = () => {
2  const grid = document.querySelector('.grid');
3  const masonry = new Masonry(grid, {
4    itemSelector: '.masonry__item'
5  });
6  // ...

Layout and Loading

When working with web pages or applications that contain images, it's important to consider scenarios where the images may not have finished loading before certain actions are performed, such as layout calculations or resizing. ImagesLoaded library simplifies this process by providing a reliable way to detect when images have been loaded or failed to load.

To use the ImagesLoaded library, you need to include the library file in your project. Once included, you can create an instance of the ImagesLoaded class, passing it a selector or an element representing the container that contains the images you want to track.

In our code example, the imagesLoaded function ensures that all the images within the grid are loaded before performing the layout for the masonry. It listens for the progress event and triggers the masonry layout. Once all the images are loaded, it adds the class is-loaded to a loader element, indicating that the gallery is ready.

1const initWannabeGallery = () => {
2  // ...
3  imagesLoaded(grid).on('progress', () => {
4    masonry.layout();
5  }).on('done', () => {
6    document.querySelector('.loader').classList.add('is-loaded');
7  });
8  // ...

Smooth Scrolling

This code sets up an event listener for both the "wheel" and "touchmove" events on the window object.

1const initWannabeGallery = () => {
2  // ...
3  const scrollTime = 0.8;
4  const scrollDistance = 240;
5  const windowScrollTop = () => window.scrollTop || document.documentElement.scrollTop;
7  window.addEventListener("wheel touchmove", (event) => {
8    event.preventDefault();
9    const delta = event.originalEvent.wheelDelta / 120 || -event.originalEvent.detail / 3;
10    const scrollTop = windowScrollTop();
11    const finalScroll = scrollTop - parseInt(delta * scrollDistance);
13, {
14      duration: scrollTime,
15      scrollTo: { y: finalScroll, autoKill: true },
16      ease: "power4.out",
17      autoKill: true,
18      overwrite: 5
19    });
20  });
21  // ...

When the user performs a scrolling action, the callback function is executed. The event's default behavior is prevented using event.preventDefault().

The code then calculates the scrolling amount based on the user's input. The delta variable is determined by dividing event.originalEvent.wheelDelta by 120 or -event.originalEvent.detail by 3, depending on which value is available.

Next, the code retrieves the current vertical scroll position of the window using windowScrollTop() and assigns it to the scrollTop variable.

Finally, the finalScroll position is calculated by subtracting the product of delta and scrollDistance from scrollTop. The scrollDistance represents the distance or speed of the scroll.

The function from the GSAP library is then called. It animates the scrolling behavior of the window object. The duration of the scroll animation is set by scrollTime. The scrollTo property specifies the target scroll position, defined by { y: finalScroll, autoKill: true }. The ease property determines the easing function for the scroll animation. The autoKill and overwrite properties define certain options and behaviors for the animation.

In summary, this code captures scrolling events and modifies the scrolling behavior of the window by animating it to a new position using the GSAP library.

Animation and Interactivity

Following code sets up animations and interactivity for each masonry item in the gallery.

1const initWannabeGallery = () => {
2  // ...
3  timelineItems.forEach((item) => {
4    const timeline = gsap.timeline().timeScale(4);
5    const background = item.querySelector('.background');
6    const h2 = item.querySelector('h2');
7    const detail = item.querySelector('.detail');
8    const overlay = document.querySelector('.close-detail-view');
10    // Animation steps
11    timeline.fromTo(item, { autoAlpha: 0.15 }, { autoAlpha: 1, duration: 0.35, ease: "power4.inOut" })
12      .set(item, { className: "+=active", duration: 0.35 })
13      // Add more animation steps here as needed
14      .pause(0)
15      .reversed(true);
17    item.animation = timeline;
19    // Overlay event listener
20    overlay.addEventListener('click', () => {
21      overlay.classList.remove('active');
22      timeline.reverse();
23    });
24  });
25  // ...

It iterates through each element in the timelineItems collection. For each item, a GSAP timeline is created with a time scale of 4. The timeline includes animation steps defined using the GSAP API.

Inside the timeline animation steps, various elements are selected using query selectors, such as background, h2, detail, and overlay. These elements represent different parts of the timeline item's content / structure.

The animation steps include fading in the item using the autoAlpha property, setting the className of the item to add the active class, and additional animation steps as needed. The animation is initially paused and set to be reversed.

The item.animation property is assigned the timeline instance, allowing it to be accessed or controlled later.

An event listener is added to the overlay element, which is representing a "close button", our close overlay element. When clicked, it removes the "active" class from the overlay element and reverses the timeline animation.

Scroll Effects

The provided code snippet demonstrates the usage of the ScrollMagic JavaScript library to create scroll-based animations for a collection of items.

1const initWannabeGallery = () => {
2  // ...
3  const controller = new ScrollMagic.Controller();
4  const scrollItems = Array.from(document.querySelectorAll('.masonry__item'));
6  scrollItems.forEach((item) => {
7    const sceneTimelineIn = gsap.timeline();
8    const sceneTimelineOut = gsap.timeline();
10    // Animation for item entering viewport
11    sceneTimelineIn.fromTo(item, { y: 120 }, { y: 0, duration: 0.35 });
13    // Animation for item leaving viewport
14    sceneTimelineOut.fromTo(item, { y: 0 }, { y: -120, duration: 0.35 });
16    const sceneIn = new ScrollMagic.Scene({
17      triggerElement: item,
18      triggerHook: "onEnter",
19      duration: window.innerHeight / 2,
20      offset: -60
21    })
22    .setTween(sceneTimelineIn)
23    .setClassToggle(item, "centered")
24    .addTo(controller);
26    const sceneOut = new ScrollMagic.Scene({
27      triggerElement: item,
28      triggerHook: "onLeave",
29      duration: window.innerHeight / 2,
30      offset: 60
31    })
32    .setTween(sceneTimelineOut)
33    .setClassToggle(item, "centered")
34    .addTo(controller);
35  });
36  // ...

The code iterates over each item in the scrollItems collection using the forEach method. The scrollItems collection represents a collection of DOM elements.

Inside the loop, two GSAP timelines are created: sceneTimelineIn and sceneTimelineOut. These timelines define animations for when the item enters and leaves the viewport, respectively.

The sceneTimelineIn animation moves the item from a vertical position of 120px to 0px, with a duration of 0.35s. This animation is triggered when the item enters the viewport.

Conversely, the sceneTimelineOut animation moves the item from its current position to a vertical position of -120px, also with a duration of 0.35s. This animation is triggered when the item leaves the viewport.

The ScrollMagic.Scene constructor is then used to create two scenes: sceneIn and sceneOut. These scenes define the trigger element, trigger hook, duration, and offset for each animation.

For sceneIn, the trigger element is set to the item itself, and the trigger hook is set to onEnter (when the element enters the viewport). The duration is calculated as half of the window's inner height, and the offset is set to -60px.

Similarly, for sceneOut, the trigger element is set to the item, the trigger hook is onLeave (when the element leaves the viewport), the duration is half of the window's inner height, and the offset is 60px.

Both scenes are then associated with their respective timelines using the setTween method. Additionally, the setClassToggle method is used to toggle the centered class on the item element as the scenes are triggered.

Finally, the scenes are added to the controller, which is an instance of the ScrollMagic controller responsible for managing the scenes and triggering animations based on the scroll position.

Interactive Detail

The code adds interactivity to the detail section of each item. When the mouse enters the detail area, an animation timeline is played, which moves and transforms the circles. When the mouse leaves, the animation is reversed.

1const initWannabeGallery = () => {
2  // ...
3  const detailActive = item.querySelector('.detail');
4  if (detailActive) {
5    const hoverTimeline = gsap.timeline({ paused: true });
6    const circle2 = detailActive.querySelector('.circle2');
7    const circle1 = detailActive.querySelector('.circle1');
9    hoverTimeline.fromTo(circle2.querySelector('span'), { x: 0 }, { x: 100, duration: 0.35, ease: "power4.inOut" })
10      .fromTo(circle1, { x: 0 }, { x: 24, duration: 0.35, ease: "power4.inOut" }, "-=.20");
12    detailActive.addEventListener("mouseover", () => {
14    });
16    detailActive.addEventListener("mouseout", () => {
17      hoverTimeline.reverse();
18    });
19  }
20  // ...

Skew effect on scroll

Lastly, we leverage GSAP's ScrollTrigger to add a subtle skew effect to the gallery items as the user scrolls.

1const initWannabeGallery = () => {
2  // ...
3  let proxy = { skew: 0 },
4      skewSetter = gsap.quickSetter(".masonry__item", "skewY", "deg"), // fast
5      clamp = gsap.utils.clamp(-20, 20); // don't let the skew go beyond 20 degrees. 
7  ScrollTrigger.create({
8    onUpdate: (self) => {
9      let skew = clamp(self.getVelocity() / -300);
10      // only do something if the skew is MORE severe. Remember, we're always tweening back to 0, so if the user slows their scrolling quickly, it's more natural to just let the tween handle that smoothly rather than jumping to the smaller skew.
11      if (Math.abs(skew) > Math.abs(proxy.skew)) {
12        proxy.skew = skew;
13, {skew: 0, duration: 0.8, ease: "power3", overwrite: true, onUpdate: () => skewSetter(proxy.skew)});
14      }
15    }
16  });
17  // ...

The provided code snippet involves the use of GSAP's ScrollTrigger library for creating scroll-based animations.

First, the code defines a variable proxy and initializes it with an object containing a skew property set to 0. It also creates a skewSetter function using GSAP's quickSetter method, which enables efficient updating of the skewY property on elements with the class masonry__item. Additionally, a clamp function is defined using GSAP's utils.clamp method, limiting the skew value between -20 and 20 degrees.

Next, the ScrollTrigger is created using ScrollTrigger.create(). Within its options, an onUpdate callback function is specified. This function is triggered whenever the scroll position is updated.

Inside the onUpdate callback, a skew value is calculated based on the scroll velocity using self.getVelocity() divided by -300. This ensures that the skew effect responds to the speed of scrolling.

If the absolute value of the calculated skew is greater than the absolute value of the current proxy.skew value, the code updates proxy.skew and uses GSAP's to() method to tween it back to 0. This tweening effect has a duration of 0.8s, an easing function of power3, and updates the skewSetter function during each step of the animation.


By combining the power of Masonry, GSAP, and ScrollMagic libraries, we have created an interactive masonry image gallery with smooth animations. The gallery provides a visually appealing and engaging experience for your website visitors. Feel free to customize the HTML structure and experiment with different animations and effects to make it truly unique.

Please ensure that you have the necessary libraries (e.g., Masonry, imagesLoaded, GSAP, ScrollMagic) included in your HTML file and properly set up for this code to work correctly.

You can find whole code in github by clicking on the GET CODE button at the beginning of the article.

Now it's time to showcase your beautiful images in a stunning gallery that captures attention and enhances user experience!


More like this

Ready for more? Here are some related posts to explore