Masonry Image Gallery with Viewer using HTML, CSS, JavaScript and gsap
Published On :- March 5, 2025 | code : | watch video tutorial :

Masonry Image Gallery with Viewer

This project is a responsive masonry-style image gallery with an interactive viewer. The gallery allows users to click on an image to open a full-screen viewer, navigate through images using next/previous buttons, and close the viewer smoothly. It is built using HTML, CSS, JavaScript, and GSAP for animations.

Final Output Preview

Before we dive into the code, here is a preview of what we will build:

Masonry Image Gallery with Viewer using HTML, CSS, JavaScript and gsap

Code Explanation (Step-by-Step)

1. HTML Structure

  • The .gallerContainer holds images in a masonry layout.
  • .galleryViewer acts as an overlay to display selected images.

<h1>masonry image gallery with viewer</h1>
        <div class="gallerContainer">
            <div class="imgBox"><img src="./images/img-1.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-2.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-3.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-4.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-5.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-6.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-7.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-8.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-9.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-10.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-11.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-12.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-13.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-14.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-15.jpg" alt=""></div>
            <div class="imgBox"><img src="./images/img-16.jpg" alt=""></div>
        </div>
        <!-- gallery viewer -->
        <div class="galleryViewer">
            <div class="imgBox"><img src="./images/img-1.jpg" alt=""></div>
            <div class="details"><span>1/4</span></div>
            <div class="arrows">
                <span class="prev"><i class="fa-solid fa-arrow-left"></i></span>
                <span class="next"><i class="fa-solid fa-arrow-right"></i></span>
            </div>
            <div class="close">
                <span><i class="fa-solid fa-xmark"></i></span>
            </div>
        </div>

2. CSS Styling

  • Uses CSS columns for masonry layout.
  • .galleryViewer is hidden by default and appears when active.

*,
            *::before,
            *::after {
                padding: 0;
                margin: 0;
                box-sizing: border-box;
            }
        
            html,
            body {
                width: 100%;
                height: 100%;
                position: relative;
            }
        
            body {
                background: linear-gradient(to right, #67b26f, #4ca2cd);
                font-family: 'poppins';
            }
        
            .no-scroll {
                overflow: hidden;
            }
        
            body h1 {
                text-align: center;
                color: white;
                text-transform: capitalize;
                margin-block: 1rem;
            }
        
            /* gallerContainer */
            .gallerContainer {
                max-width: 100%;
                width: 1000px;
                margin: 1rem auto;
                columns: 3;
                column-gap: 0.4rem;
            }
        
            .gallerContainer .imgBox {
                width: 100%;
                cursor: pointer;
                overflow: hidden;
                position: relative;
                isolation: isolate;
            }
        
            .gallerContainer .imgBox::before {
                content: "click to view";
                position: absolute;
                top: 60%;
                left: 50%;
                transform: translate(-50%, -50%);
                color: white;
                background-color: rgba(0, 0, 0, 0.351);
                text-transform: capitalize;
                font: 1.2rem;
                padding: 0.5rem;
                text-align: center;
                opacity: 0;
                visibility: hidden;
                transition: 300ms ease;
            }
        
            .gallerContainer .imgBox:hover::before {
                top: 50%;
                opacity: 1;
                visibility: visible;
            }
        
            .gallerContainer .imgBox img {
                width: 100%;
            }
        
            /* galleryViewer */
            .galleryViewer {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background-color: rgba(0, 0, 0, 0.31);
                display: none;
                justify-content: center;
                align-items: center;
                flex-direction: column;
                gap: 1rem;
                opacity: 0;
            }
        
            .galleryViewer.active {
                display: flex;
            }
        
            .galleryViewer .imgBox {
                display: flex;
                max-width: 800px;
                max-height: 600px;
                border: 2px solid white;
            }
        
            .galleryViewer .imgBox img {
                width: 100%;
                height: 100%;
                object-fit: contain;
            }
        
            .galleryViewer .details {
                color: white;
                background-color: black;
                font-size: 1.4rem;
                padding: 0.4rem;
            }
        
            .galleryViewer .arrows {
                position: absolute;
                top: 50%;
                left: 0;
                width: 100%;
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 1rem;
            }
        
            .galleryViewer .arrows span {
                background-color: white;
                font-size: 1.2rem;
                width: 45px;
                height: 45px;
                display: flex;
                justify-content: center;
                align-items: center;
                border-radius: 50%;
                cursor: pointer;
                transition: 300ms ease;
            }
        
            .galleryViewer .arrows span:hover {
                background-color: rgba(255, 255, 255, 0.371);
            }
        
            .galleryViewer .arrows span:active {
                background-color: rgba(255, 255, 255, 0.592);
                transform: scale(0.9);
            }
        
            .galleryViewer .close {
                width: 45px;
                height: 45px;
                background-color: red;
                display: flex;
                justify-content: center;
                align-items: center;
                color: white;
                cursor: pointer;
                position: absolute;
                top: 1rem;
                right: 1rem;
            }
        
            .galleryViewer .close:hover {
                background-color: rgba(255, 0, 0, 0.599);
            }

3. JavaScript Functionality

  • Uses event listeners to open/close viewer.
  • Uses GSAP for smooth animations.

document.addEventListener('DOMContentLoaded', () => {
            let galleryContainer = document.querySelector('.gallerContainer');
            let imageBoxes = galleryContainer.querySelectorAll('.imgBox')
            // gallery viewer
            let galleryViewer = document.querySelector('.galleryViewer');
            let viewerImageBox = galleryViewer.querySelector('.imgBox');
            let viewerImage = galleryViewer.querySelector('.imgBox img');
            let details = galleryViewer.querySelector('.details span');
            let nextBtn = galleryViewer.querySelector('.arrows .next');
            let prevBtn = galleryViewer.querySelector('.arrows .prev');
            let galleryCloser = galleryViewer.querySelector('.close');
    
            // slider variables
            let isAnimating = false;
            let currentSlideIndex = 0;
    
            //open gallery viewer
            imageBoxes.forEach((imgBox,index)=>{
                imgBox.addEventListener("click",()=>{
                    let img = imgBox.querySelector('img');
                    gsap.to(galleryViewer,{
                        opacity:1,
                        scale:1,
                        duration:0.3,
                        ease:"power2.in",
                        onStart:()=>{
                            galleryViewer.classList.add('active');
                            document.body.classList.add('no-scroll')
                            currentSlideIndex=index;
                        }
                    })
                    //show clicked image
                    gsap.fromTo(viewerImageBox,{
                        opacity:0,
                        scale:0.8,
                        onComplete:()=>{
                            viewerImage.src = img.src;
                            details.innerText = `${currentSlideIndex + 1}/${imageBoxes.length}`
                        }
                    },{
                        opacity:1,
                        scale:1,
                        duration:0.5,
                        ease:"power2.out"
                    })
                })
            })
    
            // to close gallery viewer
            galleryCloser.addEventListener("click",()=>{
                gsap.to(galleryViewer,{
                    opacity:0,
                    scale:0.8,
                    duration:0.3,
                    ease:"power2.in",
                    onComplete:()=>{
                        galleryViewer.classList.remove('active')
                        document.body.classList.remove('no-scroll')
                        currentSlideIndex=0;
                    }
                })
            })
    
            // slide functions
            function showSlide(isNext=true){
                isAnimating = true;
                viewerImage.src = imageBoxes[currentSlideIndex].querySelector('img').src;
                gsap.fromTo(viewerImageBox,{
                    opacity:0,
                    x:isNext ? "100%" :"-100%"
                },
            {
                opacity:1,
                x:"0%",
                duration:0.5,
                ease:"power2.out",
                onStart:()=>{
                    details.innerText = `${currentSlideIndex + 1}/${imageBoxes.length}`
                },
                onComplete:()=>{
                    isAnimating=false;
                }
            })
            }
            function nextSlide(){
                if(isAnimating) return
                currentSlideIndex = (currentSlideIndex +1)% imageBoxes.length;
                showSlide()
            }
            function prevSlide(){
                if(isAnimating) return
                currentSlideIndex = (currentSlideIndex -1 + imageBoxes.length)% imageBoxes.length;
                showSlide(false)
            }
            //call functions on click
            nextBtn.addEventListener('click',nextSlide)
            prevBtn.addEventListener('click',prevSlide)
            })

full code

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>masonry image gallery with viewer</title>
    <!-- font awesome cdn -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" />
</head>
<style>
    *,
    *::before,
    *::after {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
    }

    html,
    body {
        width: 100%;
        height: 100%;
        position: relative;
    }

    body {
        background: linear-gradient(to right, #67b26f, #4ca2cd);
        font-family: 'poppins';
    }

    .no-scroll {
        overflow: hidden;
    }

    body h1 {
        text-align: center;
        color: white;
        text-transform: capitalize;
        margin-block: 1rem;
    }

    /* gallerContainer */
    .gallerContainer {
        max-width: 100%;
        width: 1000px;
        margin: 1rem auto;
        columns: 3;
        column-gap: 0.4rem;
    }

    .gallerContainer .imgBox {
        width: 100%;
        cursor: pointer;
        overflow: hidden;
        position: relative;
        isolation: isolate;
    }

    .gallerContainer .imgBox::before {
        content: "click to view";
        position: absolute;
        top: 60%;
        left: 50%;
        transform: translate(-50%, -50%);
        color: white;
        background-color: rgba(0, 0, 0, 0.351);
        text-transform: capitalize;
        font: 1.2rem;
        padding: 0.5rem;
        text-align: center;
        opacity: 0;
        visibility: hidden;
        transition: 300ms ease;
    }

    .gallerContainer .imgBox:hover::before {
        top: 50%;
        opacity: 1;
        visibility: visible;
    }

    .gallerContainer .imgBox img {
        width: 100%;
    }

    /* galleryViewer */
    .galleryViewer {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.31);
        display: none;
        justify-content: center;
        align-items: center;
        flex-direction: column;
        gap: 1rem;
        opacity: 0;
    }

    .galleryViewer.active {
        display: flex;
    }

    .galleryViewer .imgBox {
        display: flex;
        max-width: 800px;
        max-height: 600px;
        border: 2px solid white;
    }

    .galleryViewer .imgBox img {
        width: 100%;
        height: 100%;
        object-fit: contain;
    }

    .galleryViewer .details {
        color: white;
        background-color: black;
        font-size: 1.4rem;
        padding: 0.4rem;
    }

    .galleryViewer .arrows {
        position: absolute;
        top: 50%;
        left: 0;
        width: 100%;
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 1rem;
    }

    .galleryViewer .arrows span {
        background-color: white;
        font-size: 1.2rem;
        width: 45px;
        height: 45px;
        display: flex;
        justify-content: center;
        align-items: center;
        border-radius: 50%;
        cursor: pointer;
        transition: 300ms ease;
    }

    .galleryViewer .arrows span:hover {
        background-color: rgba(255, 255, 255, 0.371);
    }

    .galleryViewer .arrows span:active {
        background-color: rgba(255, 255, 255, 0.592);
        transform: scale(0.9);
    }

    .galleryViewer .close {
        width: 45px;
        height: 45px;
        background-color: red;
        display: flex;
        justify-content: center;
        align-items: center;
        color: white;
        cursor: pointer;
        position: absolute;
        top: 1rem;
        right: 1rem;
    }

    .galleryViewer .close:hover {
        background-color: rgba(255, 0, 0, 0.599);
    }
</style>

<body>
    <h1>masonry image gallery with viewer</h1>
    <div class="gallerContainer">
        <div class="imgBox"><img src="./images/img-1.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-2.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-3.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-4.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-5.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-6.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-7.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-8.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-9.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-10.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-11.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-12.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-13.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-14.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-15.jpg" alt=""></div>
        <div class="imgBox"><img src="./images/img-16.jpg" alt=""></div>
    </div>
    <!-- gallery viewer -->
    <div class="galleryViewer">
        <div class="imgBox"><img src="./images/img-1.jpg" alt=""></div>
        <div class="details"><span>1/4</span></div>
        <div class="arrows">
            <span class="prev"><i class="fa-solid fa-arrow-left"></i></span>
            <span class="next"><i class="fa-solid fa-arrow-right"></i></span>
        </div>
        <div class="close">
            <span><i class="fa-solid fa-xmark"></i></span>
        </div>
    </div>
    <!-- gsap cdn -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
    <!-- script code -->
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            let galleryContainer = document.querySelector('.gallerContainer');
            let imageBoxes = galleryContainer.querySelectorAll('.imgBox')
            // gallery viewer
            let galleryViewer = document.querySelector('.galleryViewer');
            let viewerImageBox = galleryViewer.querySelector('.imgBox');
            let viewerImage = galleryViewer.querySelector('.imgBox img');
            let details = galleryViewer.querySelector('.details span');
            let nextBtn = galleryViewer.querySelector('.arrows .next');
            let prevBtn = galleryViewer.querySelector('.arrows .prev');
            let galleryCloser = galleryViewer.querySelector('.close');

            // slider variables
            let isAnimating = false;
            let currentSlideIndex = 0;

            //open gallery viewer
            imageBoxes.forEach((imgBox, index) => {
                imgBox.addEventListener("click", () => {
                    let img = imgBox.querySelector('img');
                    gsap.to(galleryViewer, {
                        opacity: 1,
                        scale: 1,
                        duration: 0.3,
                        ease: "power2.in",
                        onStart: () => {
                            galleryViewer.classList.add('active');
                            document.body.classList.add('no-scroll')
                            currentSlideIndex = index;
                        }
                    })
                    //show clicked image
                    gsap.fromTo(viewerImageBox, {
                        opacity: 0,
                        scale: 0.8,
                        onComplete: () => {
                            viewerImage.src = img.src;
                            details.innerText = `${currentSlideIndex + 1}/${imageBoxes.length}`
                        }
                    }, {
                        opacity: 1,
                        scale: 1,
                        duration: 0.5,
                        ease: "power2.out"
                    })
                })
            })

            // to close gallery viewer
            galleryCloser.addEventListener("click", () => {
                gsap.to(galleryViewer, {
                    opacity: 0,
                    scale: 0.8,
                    duration: 0.3,
                    ease: "power2.in",
                    onComplete: () => {
                        galleryViewer.classList.remove('active')
                        document.body.classList.remove('no-scroll')
                        currentSlideIndex = 0;
                    }
                })
            })

            // slide functions
            function showSlide(isNext = true) {
                isAnimating = true;
                viewerImage.src = imageBoxes[currentSlideIndex].querySelector('img').src;
                gsap.fromTo(viewerImageBox, {
                    opacity: 0,
                    x: isNext ? "100%" : "-100%"
                },
                    {
                        opacity: 1,
                        x: "0%",
                        duration: 0.5,
                        ease: "power2.out",
                        onStart: () => {
                            details.innerText = `${currentSlideIndex + 1}/${imageBoxes.length}`
                        },
                        onComplete: () => {
                            isAnimating = false;
                        }
                    })
            }
            function nextSlide() {
                if (isAnimating) return
                currentSlideIndex = (currentSlideIndex + 1) % imageBoxes.length;
                showSlide()
            }
            function prevSlide() {
                if (isAnimating) return
                currentSlideIndex = (currentSlideIndex - 1 + imageBoxes.length) % imageBoxes.length;
                showSlide(false)
            }
            //call functions on click
            nextBtn.addEventListener('click', nextSlide)
            prevBtn.addEventListener('click', prevSlide)
        })
    </script>
</body>

</html>

Tags : #masonry image gallery #image viewer #responsive gallery #HTML CSS JavaScript gallery #GSAP animations #image slider #photo gallery #JavaScript lightbox #interactive gallery

Recent blog Posts

search post

search video tutorial