웹솔루션개발 26년 노하우! 해피CGI의 모든것

[해피CGI][cgimall] 마우스 오버로 확장되는 벤토 그리드 갤러리 본문

웹프로그램밍 자료실/기타 자료

[해피CGI][cgimall] 마우스 오버로 확장되는 벤토 그리드 갤러리

해피CGI윤실장 2026. 10. 30. 09:15

여러 이미지를 3×3 형태의 벤토 그리드로 배치하고, 사용자가 특정 이미지를 가리키면 해당 행과 열의 비율이 변경되도록 구성된 인터랙션 예제입니다.
GSAP의 Flip 플러그인을 활용해 레이아웃이 바뀌는 과정이 끊기지 않고 부드러운 애니메이션으로 표현됩니다.

선택된 이미지 영역이 커지면서 주변 이미지들은 자연스럽게 축소·재배치되어 사용자의 시선을 집중시키는 효과를 줍니다.
단순한 이미지 확대가 아니라 그리드 전체 구조가 함께 변화하기 때문에 일반 갤러리보다 역동적인 화면 구성이 가능합니다.
포트폴리오, 브랜드 이미지 쇼케이스, 작품 갤러리, 제품 비주얼 소개 영역 등에 활용하기 좋은 인터랙션 방식입니다.

HTML 구조

<div class="bento-container cols-1-3-1 rows-1-3-1">

  <div data-column="0" data-row="0" class="bento-item">

   

    <div class="image-overlay"></div>

  </div>

  <div data-column="1" data-row="0" class="bento-item">

   

    <div class="image-overlay"></div>

  </div>

  <div data-column="2" data-row="0" class="bento-item">

   

    <div class="image-overlay"></div>

  </div>

  <div data-column="0" data-row="1" class="bento-item">

   

    <div class="image-overlay"></div>

  </div>

  <div data-column="1" data-row="1" class="bento-item center-piece">

   

    <div class="image-overlay"></div>

  </div>

  <div data-column="2" data-row="1" class="bento-item">

   

    <div class="image-overlay"></div>

  </div>

  <div data-column="0" data-row="2" class="bento-item">

   

    <div class="image-overlay"></div>

  </div>

  <div data-column="1" data-row="2" class="bento-item">

   

    <div class="image-overlay"></div>

  </div>

  <div data-column="2" data-row="2" class="bento-item">

   

    <div class="image-overlay"></div>

  </div>

</div>



CSS 소스

:root {

  --bg-color: #f0f2f5;

  --gap: 15px;

}

 

body {

  display: flex;

  justify-content: center;

  align-items: center;

  min-height: 100vh;

  margin: 0;

}

 

/* The Bento Grid Logic */

.bento-container {

  display: grid;

  gap: var(--gap);

  width: 90vw;

  height: 90vh;

  max-width: 1000px;

  max-height: 800px;

}

 

/*

Specific classes for each event target

*/

.bento-container.rows-3-1-1 {

  grid-template-rows: 3fr 1fr 1fr;

}

 

.bento-container.rows-1-3-1 {

  grid-template-rows: 1fr 3fr 1fr;

}

 

.bento-container.rows-1-1-3 {

  grid-template-rows: 1fr 1fr 3fr;

}

 

.bento-container.cols-3-1-1 {

  grid-template-columns: 3fr 1fr 1fr;

}

 

.bento-container.cols-1-3-1 {

  grid-template-columns: 1fr 3fr 1fr;

}

 

.bento-container.cols-1-1-3 {

  grid-template-columns: 1fr 1fr 3fr;

}

 

.bento-item {

  background-color: var(--card-color);

  border-radius: 12px;

  box-shadow: 0 4px 6px rgba(255, 252, 225, 0.25);

  display: flex;

  justify-content: center;

  align-items: center;

  font-weight: bold;

  color: #333;

  position: relative;

  cursor: pointer;

  overflow: hidden;

  border: 1px solid rgba(255, 252, 225, 0.5);

 

  img {

    position: absolute;

    inset: 0;

    object-fit: cover;

    width: 100%;

    height: 100%;

    z-index: 1;

  }

 

  .image-overlay {

    position: absolute;

    inset: 0;

    background-color: #333;

    opacity: 0;

    z-index: 2;

  }

}

 

/* Hover Effect: Scale up and bring to front */

.bento-item:hover {

  z-index: 10;

}

 



JS 소스

console.clear();

 

gsap.registerPlugin(Flip);

 

const bentoContainer = document.querySelector(".bento-container");

const bentoItems = bentoContainer.querySelectorAll("[data-row]");

const rowClasses = ["rows-3-1-1", "rows-1-3-1", "rows-1-1-3"];

const colClasses = ["cols-3-1-1", "cols-1-3-1", "cols-1-1-3"];

// Remember 0 index for the classes array, same for the data attribute on the HTML elements

let activeColumn = 1,

  activeRow = 1,

  doFlip = false;

 

bentoItems.forEach((e, i) => {

  const colNumber = e.dataset.column;

  const rowNumber = e.dataset.row;

  e.addEventListener("mouseenter", () => {

    // console.log("Item", i, "column", colNumber, "row", rowNumber);

    const state = Flip.getState(bentoItems);

    if (colNumber !== activeColumn) {

      bentoContainer.classList.remove(colClasses[activeColumn]);

      bentoContainer.classList.add(colClasses[colNumber]);

      activeColumn = colNumber;

      doFlip = true;

    }

    if (rowNumber !== activeRow) {

      bentoContainer.classList.remove(rowClasses[activeRow]);

      bentoContainer.classList.add(rowClasses[rowNumber]);

      activeRow = rowNumber;

      doFlip = true;

    }

    doFlip &&

      Flip.from(state, {

        duration: 0.4,

        ease: "power2.out",

        onStart: () => (doFlip = false),

        absolute: true

      });

  });

});

 

bentoContainer.addEventListener("mouseleave", () => {

  // Toggle the boolean just in case

  doFlip = true;

  // Get the state

  const state = Flip.getState(bentoItems);

 

  // Update the classes in the container

  bentoContainer.classList.remove(colClasses[activeColumn]);

  bentoContainer.classList.remove(rowClasses[activeRow]);

  bentoContainer.classList.add(colClasses[1]);

  bentoContainer.classList.add(rowClasses[1]);

 

  // Update the index values

  activeColumn = 1;

  activeRow = 1;

 

  // Run the Flip Animation

  Flip.from(state, {

    duration: 0.4,

    ease: "power2.out",

    onStart: () => (doFlip = false),

    absolute: true

  });

});

 

 

Comments