# Shutter Scroll Transition

> Scroll-driven horizontal shutters that open full-bleed images in staggered rows — cinematic chapter transitions for editorial pages.

**Definition:** Shutter Scroll Transition is a scroll 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: Scroll
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-02
- Canonical page: https://html.bosconvertpro.com/collection/shutter-scroll-transition
- Full-page preview: https://html.bosconvertpro.com/collection/shutter-scroll-transition/full
- Live demo: https://html.bosconvertpro.com/demos/shutter-scroll-transition/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/shutter-scroll-transition/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Shutter Scroll Transition?

Shutter Scroll Transition is a scroll UI animation from collections.html. Scroll-driven horizontal shutters that open full-bleed images in staggered rows — cinematic chapter transitions for editorial pages.

### What is Shutter Scroll Transition built with?

Shutter Scroll Transition 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 Shutter Scroll Transition in my project?

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

### Is Shutter Scroll Transition free to use?

Yes. The Shutter Scroll Transition 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>Shutter Scroll Transition</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/lenis@1.2.3/dist/lenis.css"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <section class="shutter-scroll__section">
      <div class="shutter-scroll__bg">
        <img
          class="shutter-scroll__bg-img"
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1920&q=85"
          alt="Mountain range above the clouds"
          width="1920"
          height="1280"
        />
      </div>
      <div class="shutter-scroll__content">
        <h1 class="shutter-scroll__h">
          Aperture<br />
          Scroll<br />
          Layers
        </h1>
      </div>
      <div
        class="shutter-scroll-transition"
        data-shutter-scroll-transition
        data-rows="16"
        data-rows-tablet="10"
        data-rows-mobile="6"
      ></div>
    </section>

    <section class="shutter-scroll__section is--white">
      <div class="shutter-scroll__content">
        <h1 class="shutter-scroll__h">
          Also works as reveal instead of cover effect
        </h1>
      </div>
    </section>

    <section class="shutter-scroll__section">
      <div
        class="shutter-scroll-transition is--top"
        data-shutter-scroll-transition
        data-mode="reveal"
        data-rows="10"
        data-scroll-start="top bottom"
        data-scroll-end="top 30%"
      ></div>
      <div class="shutter-scroll__bg">
        <img
          class="shutter-scroll__bg-img"
          src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=1920&q=85"
          alt="Sunlight over green valley landscape"
          width="1920"
          height="1280"
        />
      </div>
      <div class="shutter-scroll__content">
        <h1 class="shutter-scroll__h">Easily adapt row size and count</h1>
      </div>
      <div
        class="shutter-scroll-transition"
        data-shutter-scroll-transition
        data-shutter-height="1.5em"
        data-rows="20"
        data-rows-tablet="12"
        data-rows-mobile="8"
      ></div>
    </section>

    <section class="shutter-scroll__section is--white">
      <div class="shutter-scroll__content">
        <h1 class="shutter-scroll__h">One system, many possibilities</h1>
      </div>
    </section>

    <section class="shutter-scroll__section">
      <div
        class="shutter-scroll-transition is--top"
        data-shutter-scroll-transition
        data-mode="reveal"
        data-rows="12"
        data-rows-tablet="8"
        data-rows-mobile="6"
        data-scroll-start="top bottom"
        data-scroll-end="top 30%"
      ></div>
      <div class="shutter-scroll__bg">
        <img
          class="shutter-scroll__bg-img"
          src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1920&q=85"
          alt="Fog rolling over forested hills"
          width="1920"
          height="1280"
        />
      </div>
      <div class="shutter-scroll__content">
        <h1 class="shutter-scroll__h">
          Field notes<br />
          in motion
        </h1>
      </div>
      <div
        class="shutter-scroll-transition is--cream"
        data-shutter-scroll-transition
        data-rows="14"
        data-rows-tablet="10"
        data-rows-mobile="6"
      ></div>
    </section>

    <section class="shutter-scroll__section is--cream">
      <div class="shutter-scroll__content">
        <h1 class="shutter-scroll__h">
          Image, then words.<br />
          Cover, then reveal.
        </h1>
      </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/ScrollTrigger.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lenis@1.2.3/dist/lenis.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 {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

html.lenis,
html.lenis body {
  height: auto;
}

body {
  font-family: "Inter", system-ui, -apple-system, sans-serif;
  background: #111110;
  color: #f4f1ea;
  overflow-x: hidden;
}

img {
  display: block;
  max-width: 100%;
}

.shutter-scroll__section {
  color: #f4f1ea;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100svh;
  position: relative;
  overflow: hidden;
}

.shutter-scroll__section.is--white {
  color: #1a1916;
  background: #f4f1ea;
}

.shutter-scroll__section.is--cream {
  color: #1a1916;
  background: #d9cfc0;
}

.shutter-scroll__bg {
  z-index: 0;
  background-color: #111110;
  position: absolute;
  inset: 0;
}

.shutter-scroll__bg-img {
  opacity: 0.85;
  object-fit: cover;
  width: 100%;
  height: 100%;
}

.shutter-scroll__content {
  z-index: 1;
  position: relative;
  padding: 1.5rem;
}

.shutter-scroll__h {
  text-align: center;
  letter-spacing: -0.04em;
  max-width: 10em;
  margin: 0 auto;
  font-size: clamp(2.5rem, 8vw, 6em);
  font-weight: 400;
  line-height: 0.95;
}

.shutter-scroll-transition {
  z-index: 10;
  pointer-events: none;
  color: #f4f1ea;
  position: absolute;
  inset: auto 0% 0%;
}

.shutter-scroll-transition.is--top {
  inset: 0% 0% auto;
}

.shutter-scroll-transition.is--cream {
  color: #d9cfc0;
}

[data-shutter-scroll-panel] {
  display: flex;
  flex-direction: column;
  width: 100%;
}

[data-shutter-scroll-row] {
  height: 3em;
  width: 100%;
  background-color: currentColor;
  backface-visibility: hidden;
  will-change: transform;
}

[data-shutter-scroll-transition][data-shutter-height="1.5em"]
  [data-shutter-scroll-row] {
  height: 1.5em;
}

@media (max-width: 767px) {
  .shutter-scroll__h {
    max-width: 12em;
  }

  [data-shutter-scroll-row] {
    height: 2.25em;
  }

  [data-shutter-scroll-transition][data-shutter-height="1.5em"]
    [data-shutter-scroll-row] {
    height: 1.25em;
  }
}
````

## Source — script.js
````javascript
gsap.registerPlugin(ScrollTrigger);

const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
  lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);

function initShutterScrollTransition() {
  const defaultRows = 6;
  const defaultMode = "cover";
  const defaultScrollStart = { cover: "bottom bottom", reveal: "top bottom" };
  const defaultScrollEnd = { cover: "bottom top", reveal: "top center" };
  const defaultScrub = 0.3;
  const defaultShutterDuration = 0.1;
  const defaultStaggerAmount = 0.01;

  const panelClass = "shutter-scroll-transition__panel";
  const rowClass = "shutter-scroll-transition__row";

  const breakpoints = {
    mobile: "(max-width: 478px)",
    landscape: "(max-width: 767px)",
    tablet: "(max-width: 991px)",
  };

  const instances = [];
  let mm = null;

  function getMode(wrapper) {
    return wrapper.dataset.mode === "reveal" ? "reveal" : defaultMode;
  }

  function getRows(wrapper) {
    const base = parseInt(wrapper.dataset.rows, 10) || defaultRows;

    if (window.matchMedia(breakpoints.mobile).matches) {
      return parseInt(wrapper.dataset.rowsMobile, 10) || base;
    }
    if (window.matchMedia(breakpoints.landscape).matches) {
      return parseInt(wrapper.dataset.rowsLandscape, 10) || base;
    }
    if (window.matchMedia(breakpoints.tablet).matches) {
      return parseInt(wrapper.dataset.rowsTablet, 10) || base;
    }
    return base;
  }

  function getScrollStart(wrapper, mode) {
    return wrapper.dataset.scrollStart || defaultScrollStart[mode];
  }

  function getScrollEnd(wrapper, mode) {
    return wrapper.dataset.scrollEnd || defaultScrollEnd[mode];
  }

  function createRow() {
    const row = document.createElement("div");
    row.classList.add(rowClass);
    row.setAttribute("data-shutter-scroll-row", "");
    return row;
  }

  function buildRows(wrapper, rows) {
    const panel = document.createElement("div");
    panel.classList.add(panelClass);
    panel.setAttribute("data-shutter-scroll-panel", "");

    const fragment = document.createDocumentFragment();
    for (let r = 0; r < rows; r++) {
      fragment.appendChild(createRow());
    }
    panel.appendChild(fragment);
    wrapper.appendChild(panel);

    return { panel };
  }

  function collectRows(panel) {
    return Array.from(panel.children);
  }

  function createAnimation(wrapper, rows, section, mode) {
    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: section,
        start: getScrollStart(wrapper, mode),
        end: getScrollEnd(wrapper, mode),
        scrub: defaultScrub,
        invalidateOnRefresh: true,
      },
    });

    const fromScale = mode === "cover" ? 0 : 1;
    const toScale = mode === "cover" ? 1 : 0;
    const origin = mode === "cover" ? "bottom center" : "top center";

    gsap.set(rows, {
      scaleY: fromScale,
      transformOrigin: origin,
    });

    tl.to(rows, {
      scaleY: toScale,
      duration: defaultShutterDuration,
      stagger: { each: defaultStaggerAmount, from: "end" },
      ease: "none",
    });

    return tl;
  }

  function setupInstance(wrapper) {
    const section = wrapper.closest("section") || wrapper.parentElement;
    const rows = getRows(wrapper);
    const mode = getMode(wrapper);

    const { panel } = buildRows(wrapper, rows);
    const rowList = collectRows(panel);
    const tl = createAnimation(wrapper, rowList, section, mode);

    return { wrapper, tl };
  }

  function destroyInstance(instance) {
    if (instance.tl) {
      instance.tl.scrollTrigger?.kill();
      instance.tl.kill();
    }
    const panel = instance.wrapper.querySelector("[data-shutter-scroll-panel]");
    if (panel) panel.remove();
  }

  function buildAll() {
    const wrappers = document.querySelectorAll(
      "[data-shutter-scroll-transition]"
    );
    wrappers.forEach((wrapper) => {
      instances.push(setupInstance(wrapper));
    });
    ScrollTrigger.refresh();
  }

  function destroyAll() {
    instances.forEach(destroyInstance);
    instances.length = 0;
  }

  const wrappers = document.querySelectorAll(
    "[data-shutter-scroll-transition]"
  );
  if (!wrappers.length) return;

  mm = gsap.matchMedia();

  mm.add(
    {
      isDesktop: "(min-width: 992px)",
      isTablet: "(min-width: 768px) and (max-width: 991px)",
      isLandscape: "(min-width: 479px) and (max-width: 767px)",
      isMobile: "(max-width: 478px)",
      reduceMotion: "(prefers-reduced-motion: reduce)",
    },
    (context) => {
      if (context.conditions.reduceMotion) return;

      buildAll();

      return () => {
        destroyAll();
      };
    }
  );
}

document.addEventListener("DOMContentLoaded", () => {
  initShutterScrollTransition();
});
````
