# Collage Focus Card on Hover

> Scattered collage cards that lift into focus on hover — perfect for portfolios and editorial layouts.

**Definition:** Collage Focus Card on Hover is a cards UI animation from collections.html. It is built with HTML, CSS, JavaScript, and GSAP, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Cards
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-01
- Canonical page: https://html.bosconvertpro.com/collection/collage-focus-card-on-hover
- Full-page preview: https://html.bosconvertpro.com/collection/collage-focus-card-on-hover/full
- Live demo: https://html.bosconvertpro.com/demos/collage-focus-card-on-hover/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/collage-focus-card-on-hover/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/collage-focus-card-on-hover/full to preview the animation in full screen.
2. Copy index.html, styles.css, and script.js from the code tabs on https://html.bosconvertpro.com/collection/collage-focus-card-on-hover.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Collage Focus Card on Hover?

Collage Focus Card on Hover is a cards UI animation from collections.html. Scattered collage cards that lift into focus on hover — perfect for portfolios and editorial layouts.

### What is Collage Focus Card on Hover built with?

Collage Focus Card on Hover is built with HTML, CSS, JavaScript, and GSAP. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use Collage Focus Card on Hover in my project?

Open the animation page (https://html.bosconvertpro.com/collection/collage-focus-card-on-hover), preview it live, then copy index.html, styles.css, and script.js from the code tabs. Paste the files into your project and adjust timing, easing, and content as needed. A standalone demo runs at https://html.bosconvertpro.com/demos/collage-focus-card-on-hover/index.html.

### Is Collage Focus Card on Hover free to use?

Yes. The Collage Focus Card on Hover demo code may be used in personal and commercial projects. You may not resell or redistribute the collection itself as a competing template pack. Full terms: https://html.bosconvertpro.com/terms.

## Source — index.html
````html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Collage Focus Card on Hover</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <section class="demo-section">
      <div data-interactive-collage-init class="interactive-collage">
        <div class="interactive-collage__collection">
          <div data-interactive-collage-list class="interactive-collage__list">
            <!-- 1 · Ocean dunes (landscape) -->
            <div data-interactive-collage-item class="interactive-collage__item is--1">
              <div data-interactive-collage-item-inner class="interactive-collage__item-inner">
                <div class="demo-card is--landscape">
                  <img
                    src="https://images.unsplash.com/photo-1473580044384-7ba9967e16a0?auto=format&fit=crop&w=900&q=80"
                    data-fallback="https://picsum.photos/seed/collage-ocean/900/675"
                    alt="Serene desert dunes at dusk"
                    class="cover-image"
                    loading="lazy"
                    draggable="false"
                  />
                </div>
              </div>
            </div>

            <!-- 2 · Woman portrait looking up (square) -->
            <div data-interactive-collage-item class="interactive-collage__item is--2">
              <div data-interactive-collage-item-inner class="interactive-collage__item-inner">
                <div class="demo-card">
                  <img
                    src="https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?auto=format&fit=crop&w=800&q=80"
                    data-fallback="https://picsum.photos/seed/collage-portrait/800/800"
                    alt="Elegant portrait in minimalist style"
                    class="cover-image"
                    loading="lazy"
                    draggable="false"
                  />
                </div>
              </div>
            </div>

            <!-- 3 · Green leaf close-up (landscape) -->
            <div data-interactive-collage-item class="interactive-collage__item is--3">
              <div data-interactive-collage-item-inner class="interactive-collage__item-inner">
                <div class="demo-card is--landscape">
                  <img
                    src="https://images.unsplash.com/photo-1518531933037-91b2f5f229cc?auto=format&fit=crop&w=900&q=80"
                    data-fallback="https://picsum.photos/seed/collage-leaf/900/675"
                    alt="Textured green leaves with soft shadows"
                    class="cover-image"
                    loading="lazy"
                    draggable="false"
                  />
                </div>
              </div>
            </div>

            <!-- 4 · Rock / canyon surface (portrait) -->
            <div data-interactive-collage-item class="interactive-collage__item is--4">
              <div data-interactive-collage-item-inner class="interactive-collage__item-inner">
                <div class="demo-card is--portrait">
                  <img
                    src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=700&q=80"
                    data-fallback="https://picsum.photos/seed/collage-rock/700/933"
                    alt="Textured rock surface with warm light"
                    class="cover-image"
                    loading="lazy"
                    draggable="false"
                  />
                </div>
              </div>
            </div>

            <!-- 5 · Beauty close-up (square) -->
            <div data-interactive-collage-item class="interactive-collage__item is--5">
              <div data-interactive-collage-item-inner class="interactive-collage__item-inner">
                <div class="demo-card">
                  <img
                    src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=800&q=80"
                    data-fallback="https://picsum.photos/seed/collage-beauty/800/800"
                    alt="Close-up portrait emphasizing natural beauty"
                    class="cover-image"
                    loading="lazy"
                    draggable="false"
                  />
                </div>
              </div>
            </div>

            <!-- 6 · Soft pink / evening sky (landscape) -->
            <div data-interactive-collage-item class="interactive-collage__item is--6">
              <div data-interactive-collage-item-inner class="interactive-collage__item-inner">
                <div class="demo-card is--landscape">
                  <img
                    src="https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?auto=format&fit=crop&w=900&q=80"
                    data-fallback="https://picsum.photos/seed/collage-clouds/900/675"
                    alt="Soft pink and cream cloudscape"
                    class="cover-image"
                    loading="lazy"
                    draggable="false"
                  />
                </div>
              </div>
            </div>

            <!-- 7 · Cream / pastel abstract (landscape) -->
            <div data-interactive-collage-item class="interactive-collage__item is--7">
              <div data-interactive-collage-item-inner class="interactive-collage__item-inner">
                <div class="demo-card is--landscape">
                  <img
                    src="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=900&q=80"
                    data-fallback="https://picsum.photos/seed/collage-swirl/900/675"
                    alt="Elegant cream abstract on soft blue"
                    class="cover-image"
                    loading="lazy"
                    draggable="false"
                  />
                </div>
              </div>
            </div>

            <!-- 8 · Face / portrait low-key (square) -->
            <div data-interactive-collage-item class="interactive-collage__item is--8">
              <div data-interactive-collage-item-inner class="interactive-collage__item-inner">
                <div class="demo-card">
                  <img
                    src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=800&q=80"
                    data-fallback="https://picsum.photos/seed/collage-water/800/800"
                    alt="Serene freckled face portrait"
                    class="cover-image"
                    loading="lazy"
                    draggable="false"
                  />
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/CustomEase.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
````

## Source — styles.css
````css
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
}

body {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background-color: #ebe5dd;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;
}

img {
  display: block;
  max-width: 100%;
  user-select: none;
  -webkit-user-drag: none;
}

/* --------------------------------------------------------------------------
   Stage
   -------------------------------------------------------------------------- */

.demo-section {
  background-color: #ebe5dd;
  justify-content: center;
  align-items: center;
  min-height: 100svh;
  font-size: 1vw;
  display: flex;
  overflow: clip;
}

/* --------------------------------------------------------------------------
   Collage shell — fixed em-based layout (scales with font-size / viewport)
   -------------------------------------------------------------------------- */

.interactive-collage {
  width: 63.75em;
  height: 37em;
  flex: none;
  cursor: default;
}

.interactive-collage__collection {
  width: 100%;
  height: 100%;
}

.interactive-collage__list {
  width: 100%;
  height: 100%;
  position: relative;
}

.interactive-collage__item {
  z-index: 0;
  position: absolute;
  top: 0;
  left: 0;
  cursor: pointer;
  /* Isolate paint so GPU can composite each card independently */
  contain: layout style;
  isolation: isolate;
}

.interactive-collage__item.is--1 {
  top: 6em;
}

.interactive-collage__item.is--2 {
  top: 17em;
  left: 5em;
}

.interactive-collage__item.is--3 {
  z-index: 1;
  top: 14em;
  left: 13.5em;
}

.interactive-collage__item.is--4 {
  left: 24em;
}

.interactive-collage__item.is--5 {
  z-index: 3;
  top: auto;
  bottom: 0;
  left: 26em;
}

.interactive-collage__item.is--6 {
  z-index: 2;
  top: 9em;
  left: 31.5em;
}

.interactive-collage__item.is--7 {
  z-index: 1;
  top: 16em;
  left: 39em;
}

.interactive-collage__item.is--8 {
  top: 5em;
  left: auto;
  right: 0;
}

/* Focused card rises above siblings (instant z so it never paints under) */
.interactive-collage__item[data-interactive-collage-focus] {
  z-index: 10;
}

/* --------------------------------------------------------------------------
   Inner wrapper — GSAP animates xPercent / yPercent / scale here
   -------------------------------------------------------------------------- */

.interactive-collage__item-inner {
  transform-origin: center center;
  will-change: transform;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  /* Promote to its own layer once, avoid thrashing */
  transform: translateZ(0);
}

/* --------------------------------------------------------------------------
   Cards
   -------------------------------------------------------------------------- */

.demo-card {
  aspect-ratio: 1;
  width: 16.25em;
  position: relative;
  overflow: hidden;
  background-color: #d4cdc3;
  box-shadow:
    0 0.15em 0.6em rgba(40, 30, 20, 0.08),
    0 0.4em 1.2em rgba(40, 30, 20, 0.06);
  /* Soft shadow lift while focused — CSS only, no layout cost */
  transition: box-shadow 1s cubic-bezier(0.16, 1, 0.3, 1);
}

.interactive-collage__item[data-interactive-collage-focus] .demo-card {
  box-shadow:
    0 0.35em 1em rgba(40, 30, 20, 0.12),
    0 0.9em 2.4em rgba(40, 30, 20, 0.14);
}

.demo-card.is--landscape {
  aspect-ratio: 4 / 3;
  width: 20em;
}

.demo-card.is--portrait {
  aspect-ratio: 3 / 4;
  width: 15em;
}

.cover-image {
  object-fit: cover;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  will-change: transform;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transform: translateZ(0);
}

/* --------------------------------------------------------------------------
   Responsive — larger em base on tablet / mobile
   -------------------------------------------------------------------------- */

@media screen and (max-width: 991px) {
  .demo-section {
    font-size: 2.125vw;
  }
}

@media screen and (max-width: 600px) {
  .demo-section {
    font-size: 2.6vw;
    padding: 1.5rem;
  }

  body {
    overflow: auto;
  }
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  .interactive-collage__item-inner {
    transition: none !important;
  }
}
````

## Source — script.js
````javascript
/**
 * Collage Focus Card on Hover — ultra-smooth edition
 * GSAP quickTo retargeting + soft ease + rAF pointer throttle
 */

(() => {
  "use strict";

  if (typeof gsap === "undefined") {
    console.error("[Collage] GSAP failed to load from CDN.");
    return;
  }

  /* Silky decelerate — long soft tail, no harsh stop */
  if (typeof CustomEase !== "undefined") {
    gsap.registerPlugin(CustomEase);
    CustomEase.create("silk", "0.16, 1, 0.3, 1");
    CustomEase.create("silkSoft", "0.22, 1, 0.36, 1");
  }

  gsap.config({ force3D: true, nullTargetWarn: false });
  gsap.ticker.lagSmoothing(500, 33);

  /* Image fallbacks if Unsplash is blocked */
  document.querySelectorAll("img.cover-image[data-fallback]").forEach((img) => {
    img.addEventListener(
      "error",
      function onErr() {
        const fb = img.getAttribute("data-fallback");
        if (fb && img.src !== fb) img.src = fb;
        img.removeEventListener("error", onErr);
      },
      { once: true }
    );
  });

  function initCollageFocusCardOnHover() {
    const activeScale = 1.08;
    const inactiveScale = 0.9;
    const gapPercent = 3;
    const secondCardBoost = 1.35;
    const centerPullPercent = 25;

    /* Longer + softer = creamier motion when retargeting mid-tween */
    const moveDuration = 1.05;
    const scaleDuration = 1.15;
    const ease =
      typeof CustomEase !== "undefined" ? "silk" : "expo.out";
    const scaleEase =
      typeof CustomEase !== "undefined" ? "silkSoft" : "power3.out";

    document.querySelectorAll("[data-interactive-collage-init]").forEach((root) => {
      root._interactiveCollageAbort?.abort();

      const list = root.querySelector("[data-interactive-collage-list]");
      const items = [...root.querySelectorAll("[data-interactive-collage-item]")];
      const isTouch = !window.matchMedia("(hover: hover) and (pointer: fine)").matches;

      if (!list || !items.length) return;

      const controller = new AbortController();
      const { signal } = controller;
      let activeItem = null;
      let leaveTimer = null;

      /**
       * quickTo: high-frequency retarget without killing inertia —
       * the key to butter-smooth hover switching.
       */
      const drivers = new Map();

      items.forEach((item) => {
        const inner = item.querySelector("[data-interactive-collage-item-inner]");
        if (!inner) return;

        gsap.set(inner, {
          xPercent: 0,
          yPercent: 0,
          scale: 1,
          force3D: true,
          transformOrigin: "50% 50%",
        });

        const img = inner.querySelector(".cover-image");
        if (img) {
          gsap.set(img, { scale: 1, force3D: true, transformOrigin: "50% 50%" });
        }

        drivers.set(item, {
          inner,
          img,
          x: gsap.quickTo(inner, "xPercent", {
            duration: moveDuration,
            ease,
          }),
          y: gsap.quickTo(inner, "yPercent", {
            duration: moveDuration,
            ease,
          }),
          s: gsap.quickTo(inner, "scale", {
            duration: scaleDuration,
            ease: scaleEase,
          }),
          imgS: img
            ? gsap.quickTo(img, "scale", {
                duration: scaleDuration * 1.1,
                ease: scaleEase,
              })
            : null,
        });
      });

      const getMoveStrength = (distance) => {
        const strength = 1 / (1 + Math.pow(distance - 1, 1.2) * 0.45);
        return distance === 2 ? strength * secondCardBoost : strength;
      };

      const animateItem = (item, xPercent, yPercent, scale, imgScale = 1) => {
        const d = drivers.get(item);
        if (!d) return;
        d.x(xPercent);
        d.y(yPercent);
        d.s(scale);
        if (d.imgS) d.imgS(imgScale);
      };

      const resetCollage = () => {
        activeItem = null;
        items.forEach((item) => {
          item.removeAttribute("data-interactive-collage-focus");
          animateItem(item, 0, 0, 1, 1);
        });
      };

      const focusItem = (active) => {
        if (activeItem === active) return;
        activeItem = active;

        items.forEach((item) => {
          if (item === active) {
            item.setAttribute("data-interactive-collage-focus", "");
          } else {
            item.removeAttribute("data-interactive-collage-focus");
          }
        });

        const listRect = list.getBoundingClientRect();
        const listCenterY = listRect.top + listRect.height / 2;
        const gap = (listRect.width * gapPercent) / 100;

        const orderedItems = [...items].sort((a, b) => {
          const aRect = a.getBoundingClientRect();
          const bRect = b.getBoundingClientRect();
          return (
            aRect.left +
            aRect.width / 2 -
            (bRect.left + bRect.width / 2)
          );
        });

        const activeIndex = orderedItems.indexOf(active);
        const activeRect = active.getBoundingClientRect();
        const activeCenterX = activeRect.left + activeRect.width / 2;
        const activeLeft = activeCenterX - (activeRect.width * activeScale) / 2;
        const activeRight = activeCenterX + (activeRect.width * activeScale) / 2;

        const leftItem = orderedItems[activeIndex - 1];
        const rightItem = orderedItems[activeIndex + 1];

        let leftMove = 0;
        let rightMove = 0;

        if (leftItem) {
          const rect = leftItem.getBoundingClientRect();
          const itemRight =
            rect.left + rect.width / 2 + (rect.width * inactiveScale) / 2;
          leftMove = Math.min(0, activeLeft - gap - itemRight);
        }

        if (rightItem) {
          const rect = rightItem.getBoundingClientRect();
          const itemLeft =
            rect.left + rect.width / 2 - (rect.width * inactiveScale) / 2;
          rightMove = Math.max(0, activeRight + gap - itemLeft);
        }

        orderedItems.forEach((item, index) => {
          if (item === active) {
            /* Focused: gentle lift + subtle Ken-Burns zoom inside */
            animateItem(item, 0, 0, activeScale, 1.06);
            return;
          }

          const rect = item.getBoundingClientRect();
          const difference = index - activeIndex;
          const distance = Math.abs(difference);
          const strength = getMoveStrength(distance);
          const itemCenterY = rect.top + rect.height / 2;
          const centerProgress =
            (listCenterY - itemCenterY) / (listRect.height / 2);
          const moveX =
            difference < 0 ? leftMove * strength : rightMove * strength;
          const scale = inactiveScale - (1 - strength) * 0.12;

          animateItem(
            item,
            (moveX / rect.width) * 100,
            centerPullPercent * centerProgress * strength,
            scale,
            1
          );
        });
      };

      /**
       * Prefer the currently focused card while pointer stays over its
       * transformed bounds — stops flicker when neighbors slide under cursor.
       */
      const getHoveredItem = (event) => {
        if (activeItem) {
          const d = drivers.get(activeItem);
          const el = d?.inner || activeItem;
          const rect = el.getBoundingClientRect();
          /* Slightly expanded hit area = fewer accidental resets */
          const pad = 6;
          if (
            event.clientX >= rect.left - pad &&
            event.clientX <= rect.right + pad &&
            event.clientY >= rect.top - pad &&
            event.clientY <= rect.bottom + pad
          ) {
            return activeItem;
          }
        }
        return (
          document
            .elementFromPoint(event.clientX, event.clientY)
            ?.closest("[data-interactive-collage-item]") || null
        );
      };

      if (isTouch) {
        items.forEach((item) => {
          item.addEventListener(
            "click",
            (event) => {
              event.stopPropagation();
              activeItem === item ? resetCollage() : focusItem(item);
            },
            { signal }
          );
        });

        document.addEventListener(
          "click",
          (event) => {
            if (!root.contains(event.target)) resetCollage();
          },
          { signal }
        );
      } else {
        /* One hover evaluation per animation frame — no pointer spam jank */
        let pending = null;
        let rafId = 0;

        const flushPointer = () => {
          rafId = 0;
          if (!pending) return;
          const event = pending;
          pending = null;

          const item = getHoveredItem(event);
          if (item) {
            if (leaveTimer) {
              clearTimeout(leaveTimer);
              leaveTimer = null;
            }
            focusItem(item);
          } else {
            /* Tiny delay before reset so gaps between cards feel continuous */
            if (!leaveTimer) {
              leaveTimer = setTimeout(() => {
                leaveTimer = null;
                resetCollage();
              }, 48);
            }
          }
        };

        root.addEventListener(
          "pointermove",
          (event) => {
            pending = event;
            if (!rafId) rafId = requestAnimationFrame(flushPointer);
          },
          { signal, passive: true }
        );

        root.addEventListener(
          "pointerleave",
          () => {
            pending = null;
            if (rafId) {
              cancelAnimationFrame(rafId);
              rafId = 0;
            }
            if (leaveTimer) clearTimeout(leaveTimer);
            leaveTimer = setTimeout(() => {
              leaveTimer = null;
              resetCollage();
            }, 60);
          },
          { signal }
        );

        signal.addEventListener("abort", () => {
          if (rafId) cancelAnimationFrame(rafId);
          if (leaveTimer) clearTimeout(leaveTimer);
        });
      }

      root._interactiveCollageAbort = controller;
    });
  }

  const boot = () => initCollageFocusCardOnHover();

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();
````
