# Hover Image Expandable

> Image strips that expand into a full panel on hover — smooth accordion gallery for photography and case studies.

**Definition:** Hover Image Expandable 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/hover-image-expandable
- Full-page preview: https://html.bosconvertpro.com/collection/hover-image-expandable/full
- Live demo: https://html.bosconvertpro.com/demos/hover-image-expandable/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/hover-image-expandable/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/hover-image-expandable/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/hover-image-expandable.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Hover Image Expandable?

Hover Image Expandable is a cards UI animation from collections.html. Image strips that expand into a full panel on hover — smooth accordion gallery for photography and case studies.

### What is Hover Image Expandable built with?

Hover Image Expandable 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 Hover Image Expandable in my project?

Open the animation page (https://html.bosconvertpro.com/collection/hover-image-expandable), 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/hover-image-expandable/index.html.

### Is Hover Image Expandable free to use?

Yes. The Hover Image Expandable 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>Hover Image Expandable</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600;700&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <main class="stage">
      <!-- Double rail BEHIND images (full viewport height, center of active panel) -->
      <div class="rails" data-rails aria-hidden="true">
        <span class="rails__line"></span>
        <span class="rails__line"></span>
      </div>

      <!-- Image accordion -->
      <div class="gallery" data-gallery></div>

      <!-- Corners + nav ON TOP of images -->
      <div class="chrome" data-chrome>
        <div class="chrome__box" data-box>
          <span class="chrome__corner chrome__corner--tl"></span>
          <span class="chrome__corner chrome__corner--tr"></span>
          <span class="chrome__corner chrome__corner--bl"></span>
          <span class="chrome__corner chrome__corner--br"></span>
        </div>
        <div class="chrome__meta" data-meta>
          <div class="chrome__name">
            <span>Abbos</span>
            <span>Nurgulshanov</span>
          </div>
          <nav class="chrome__nav">
            <a href="mailto:abbosnurgulshanov@gmail.com">EMAIL</a>
            <a
              href="https://instagram.com/abbosdesign"
              target="_blank"
              rel="noopener noreferrer"
              >INSTAGRAM</a
            >
          </nav>
        </div>
      </div>
    </main>

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

## Source — styles.css
````css
/* ==========================================================================
   Hover Image Expandable
   - Center double rails BEHIND images
   - Smooth GSAP accordion
   - Corners + nav above images
   ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
  font-family: "Inter", system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
}

.stage {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(16px, 3vw, 40px);
  overflow: hidden;
}

/* ── Rails — behind images ───────────────────────────────────────────────── */

.rails {
  --line: #ffffff;
  --line-w: 1.5px;
  --rail-gap: 5px;

  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 100%;
  z-index: 0; /* BEHIND gallery */
  pointer-events: none;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  justify-content: center;
  gap: var(--rail-gap);
  will-change: transform, width;
}

.rails__line {
  display: block;
  width: var(--line-w);
  height: 100%;
  background: var(--line);
  opacity: 0.95;
  flex: 0 0 auto;
}

/* ── Gallery — above rails ───────────────────────────────────────────────── */

.gallery {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: stretch;
  gap: 6px;
  width: min(1400px, 100%);
  height: min(72vh, 620px);
}

.panel {
  position: relative;
  flex: 0 0 auto;
  height: 100%;
  overflow: hidden;
  border-radius: 0;
  cursor: pointer;
  background: #0c0c0c;
  will-change: width;
  transform: translateZ(0);
  backface-visibility: hidden;
}

.panel:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.85);
  outline-offset: 3px;
  z-index: 3;
}

.panel__media {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: 0;
}

.panel__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
  border-radius: 0;
  transform: scale(1.08);
  filter: grayscale(1) brightness(0.82);
  user-select: none;
  pointer-events: none;
  will-change: transform, filter;
}

.panel.is-active .panel__img {
  filter: grayscale(0) brightness(1);
}

/* ── Chrome — corners + nav above images ─────────────────────────────────── */

.chrome {
  --line: #ffffff;
  --corner: 22px;

  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 100%;
  z-index: 4;
  pointer-events: none;
  will-change: transform, width;
}

.chrome__box {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
}

.chrome__corner {
  position: absolute;
  width: var(--corner);
  height: var(--corner);
  border: 0 solid var(--line);
  opacity: 0.95;
}

.chrome__corner--tl {
  top: 0;
  left: 0;
  border-top-width: 2px;
  border-left-width: 2px;
}

.chrome__corner--tr {
  top: 0;
  right: 0;
  border-top-width: 2px;
  border-right-width: 2px;
}

.chrome__corner--bl {
  bottom: 0;
  left: 0;
  border-bottom-width: 2px;
  border-left-width: 2px;
}

.chrome__corner--br {
  bottom: 0;
  right: 0;
  border-bottom-width: 2px;
  border-right-width: 2px;
}

.chrome__meta {
  position: absolute;
  left: 50%;
  transform: translateX(12px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
  pointer-events: auto;
  color: #fff;
}

.chrome__name {
  display: flex;
  flex-direction: column;
  gap: 7px;
  user-select: none;
  margin-bottom: 4px;
}

.chrome__name span {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  line-height: 1.1;
  text-transform: uppercase;
  opacity: 0.92;
}

.chrome__nav {
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.chrome__nav a {
  color: #fff;
  text-decoration: none;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  line-height: 1.1;
  text-transform: uppercase;
  opacity: 0.92;
  transition: opacity 0.2s ease;
}

.chrome__nav a:hover {
  opacity: 1;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ── Responsive ──────────────────────────────────────────────────────────── */

@media (max-width: 720px) {
  .gallery {
    gap: 4px;
    height: min(58vh, 460px);
  }

  .rails {
    --rail-gap: 4px;
  }

  .chrome {
    --corner: 14px;
  }

  .chrome__nav a {
    font-size: 0.62rem;
    letter-spacing: 0.1em;
  }

  .chrome__meta {
    gap: 6px;
  }
}
````

## Source — script.js
````javascript
/**
 * Hover Image Expandable
 * - Center double rails BEHIND images
 * - Extra-smooth GSAP accordion
 * - More images
 */

(() => {
  "use strict";

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

  /* More images — Unsplash + Picsum fallback */
  const IMAGES = [
    {
      src: "https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-01/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-02/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-03/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-04/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-05/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1452587925148-ce544e77e70d?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-06/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1563089145-599997674d42?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-07/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-08/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-09/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1469334031218-e382a71b716b?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-10/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-11/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-12/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-13/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-14/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-15/1400/900",
    },
    {
      src: "https://images.unsplash.com/photo-1514525253161-7a46d19cd819?auto=format&fit=crop&w=1400&q=80",
      fallback: "https://picsum.photos/seed/exp-16/1400/900",
    },
  ];

  /* Smoother motion — longer, softer ease (phlntn-like) */
  const COLLAPSED = 0.055;
  const EXPANDED = 1;
  const DURATION = 1.15;
  const EASE = "power4.out";

  const stage = document.querySelector(".stage");
  const gallery = document.querySelector("[data-gallery]");
  const rails = document.querySelector("[data-rails]");
  const chrome = document.querySelector("[data-chrome]");
  const box = document.querySelector("[data-box]");
  const meta = document.querySelector("[data-meta]");

  if (!stage || !gallery || !rails || !chrome || !box || !meta) return;

  /** @type {HTMLElement[]} */
  const panels = [];
  let activeIndex = 0;
  let galleryWidth = 0;
  let gap = 0;
  let layoutTween = null;

  function createPanel(item, index) {
    const panel = document.createElement("article");
    panel.className = "panel";
    panel.dataset.index = String(index);
    panel.setAttribute("role", "button");
    panel.setAttribute("tabindex", "0");
    panel.setAttribute("aria-label", `Image ${index + 1}`);
    panel.setAttribute("aria-expanded", index === 0 ? "true" : "false");

    panel.innerHTML = `
      <div class="panel__media">
        <img
          class="panel__img"
          src="${item.src}"
          alt=""
          width="1400"
          height="900"
          draggable="false"
          loading="${index < 4 ? "eager" : "lazy"}"
          decoding="async"
        />
      </div>
    `;

    const img = panel.querySelector(".panel__img");
    let tried = false;
    img.addEventListener("error", () => {
      if (tried) return;
      tried = true;
      img.src = item.fallback;
    });

    return panel;
  }

  function mount() {
    const frag = document.createDocumentFragment();
    IMAGES.forEach((item, i) => {
      const panel = createPanel(item, i);
      if (i === 0) panel.classList.add("is-active");
      panels.push(panel);
      frag.appendChild(panel);
    });
    gallery.appendChild(frag);

    // GPU-friendly defaults
    gsap.set(panels, { force3D: true });
    gsap.set([rails, chrome], { force3D: true });

    measure();
    applyLayout(true);
    bindEvents();
  }

  function measure() {
    const styles = getComputedStyle(gallery);
    gap = parseFloat(styles.columnGap || styles.gap) || 0;
    galleryWidth = gallery.clientWidth;
  }

  function sharesToWidths(active) {
    const n = panels.length;
    const totalGap = gap * Math.max(0, n - 1);
    const usable = Math.max(0, galleryWidth - totalGap);
    const totalShare = EXPANDED + COLLAPSED * (n - 1);
    return panels.map((_, i) => {
      const share = i === active ? EXPANDED : COLLAPSED;
      return (share / totalShare) * usable;
    });
  }

  function galleryOffsetInStage() {
    const s = stage.getBoundingClientRect();
    const g = gallery.getBoundingClientRect();
    return {
      left: g.left - s.left,
      top: g.top - s.top,
      height: g.height,
    };
  }

  function applyLayout(immediate) {
    const widths = sharesToWidths(activeIndex);
    const duration = immediate ? 0 : DURATION;
    const off = galleryOffsetInStage();

    // Kill previous coordinated tween so rapid hover stays smooth
    if (layoutTween) layoutTween.kill();

    panels.forEach((panel, i) => {
      const isActive = i === activeIndex;
      panel.classList.toggle("is-active", isActive);
      panel.setAttribute("aria-expanded", isActive ? "true" : "false");
    });

    // One timeline keeps widths + rails + chrome in perfect sync
    layoutTween = gsap.timeline({ defaults: { ease: EASE, duration, overwrite: "auto" } });

    panels.forEach((panel, i) => {
      layoutTween.to(
        panel,
        { width: widths[i], force3D: true },
        0
      );
      layoutTween.to(
        panel.querySelector(".panel__img"),
        {
          scale: i === activeIndex ? 1 : 1.1,
          duration: duration * 1.05,
          ease: EASE,
        },
        0
      );
    });

    // Rails BEHIND — same track as active panel (centered double line via flex)
    let x = 0;
    for (let i = 0; i < activeIndex; i++) x += widths[i] + gap;
    const w = widths[activeIndex];
    const trackX = off.left + x;

    layoutTween.to(
      rails,
      { x: trackX, width: w, force3D: true },
      0
    );

    // Chrome ON TOP — corners + meta
    gsap.set(box, { top: off.top, height: off.height });
    gsap.set(meta, { top: Math.max(12, off.top - 108) });

    layoutTween.to(
      chrome,
      { x: trackX, width: w, force3D: true },
      0
    );
  }

  function setActive(index) {
    if (index < 0 || index >= panels.length) return;
    if (index === activeIndex) return;
    activeIndex = index;
    applyLayout(false);
  }

  function navigate(delta) {
    setActive(Math.min(panels.length - 1, Math.max(0, activeIndex + delta)));
  }

  function bindEvents() {
    panels.forEach((panel, i) => {
      panel.addEventListener("pointerenter", (e) => {
        if (e.pointerType === "touch") return;
        setActive(i);
      });
      panel.addEventListener("focus", () => setActive(i));
      panel.addEventListener("click", () => setActive(i));

      panel.addEventListener("keydown", (e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          setActive(i);
        }
        if (e.key === "ArrowRight" || e.key === "ArrowDown") {
          e.preventDefault();
          navigate(1);
          panels[activeIndex].focus({ preventScroll: true });
        }
        if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
          e.preventDefault();
          navigate(-1);
          panels[activeIndex].focus({ preventScroll: true });
        }
      });
    });

    let resizeTimer = 0;
    window.addEventListener("resize", () => {
      clearTimeout(resizeTimer);
      resizeTimer = window.setTimeout(() => {
        measure();
        applyLayout(true);
      }, 80);
    });

    let startX = 0;
    let startY = 0;
    gallery.addEventListener(
      "touchstart",
      (e) => {
        startX = e.changedTouches[0].clientX;
        startY = e.changedTouches[0].clientY;
      },
      { passive: true }
    );
    gallery.addEventListener(
      "touchend",
      (e) => {
        const dx = e.changedTouches[0].clientX - startX;
        const dy = e.changedTouches[0].clientY - startY;
        if (Math.abs(dx) < 40 || Math.abs(dx) < Math.abs(dy)) return;
        navigate(dx < 0 ? 1 : -1);
      },
      { passive: true }
    );
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", mount, { once: true });
  } else {
    mount();
  }
})();
````
