# Arc Scroll Transition

> Scroll-driven SVG arc wipes that cover or reveal full-bleed sections with tunable curve depth — cinematic chapter transitions.

**Definition:** Arc 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-30
- Canonical page: https://html.bosconvertpro.com/collection/arc-scroll-transition
- Full-page preview: https://html.bosconvertpro.com/collection/arc-scroll-transition/full
- Live demo: https://html.bosconvertpro.com/demos/arc-scroll-transition/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/arc-scroll-transition/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Arc Scroll Transition?

Arc Scroll Transition is a scroll UI animation from collections.html. Scroll-driven SVG arc wipes that cover or reveal full-bleed sections with tunable curve depth — cinematic chapter transitions.

### What is Arc Scroll Transition built with?

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

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

### Is Arc Scroll Transition free to use?

Yes. The Arc 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>Arc 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&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="arc-scroll__section">
      <div class="arc-scroll__bg">
        <img
          class="arc-scroll__bg-img"
          src="https://images.unsplash.com/photo-1613490493576-7fde63acd811?auto=format&fit=crop&w=1920&q=85"
          alt="Sunlit modern villa with palm shadows on the facade"
          width="1920"
          height="1280"
        />
      </div>
      <div class="arc-scroll__content">
        <h1 class="arc-scroll__h">
          Arc<br />
          Scroll<br />
          Transition
        </h1>
      </div>
      <div data-arc-scroll-transition class="arc-scroll-transition"></div>
    </section>

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

    <section class="arc-scroll__section">
      <div
        class="arc-scroll-transition is--top"
        data-arc-scroll-transition
        data-mode="reveal"
        data-curve="10"
      ></div>
      <div class="arc-scroll__bg">
        <img
          class="arc-scroll__bg-img"
          src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1920&q=85"
          alt="Courtyard pool with palm shadows on stone walls"
          width="1920"
          height="1280"
        />
      </div>
      <div class="arc-scroll__content">
        <h1 class="arc-scroll__h">Easily adapt how steep the curve is</h1>
      </div>
      <div
        class="arc-scroll-transition"
        data-arc-scroll-transition
        data-curve="25"
      ></div>
    </section>

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

    <section class="arc-scroll__section">
      <div class="arc-scroll__bg">
        <img
          class="arc-scroll__bg-img"
          src="https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1920&q=85"
          alt="Desert villa exterior at golden hour"
          width="1920"
          height="1280"
        />
      </div>
      <div class="arc-scroll__content">
        <h1 class="arc-scroll__h">
          Arc<br />
          Scroll<br />
          Transition
        </h1>
      </div>
      <div
        class="arc-scroll-transition is--top"
        data-arc-scroll-transition
        data-mode="reveal"
        data-curve="5"
      ></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 {
  color: #201d1d;
  background-color: #f4f4f4;
  font-family: Inter, Arial, sans-serif;
  font-size: 1em;
  font-weight: 400;
  line-height: 1.25;
  overflow-x: hidden;
}

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

.arc-scroll__section {
  color: #f2f2f2;
  flex-flow: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  min-height: 100svh;
  display: flex;
  position: relative;
  overflow: hidden;
}

.arc-scroll__section.is--white {
  color: #0a0a0a;
  background-color: #cecece;
}

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

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

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

.arc-scroll__h {
  text-align: center;
  letter-spacing: -0.04em;
  max-width: 8em;
  margin-top: 0;
  margin-bottom: 0;
  font-family: Inter, Arial, sans-serif;
  font-size: 6em;
  font-weight: 400;
  line-height: 0.95;
}

.arc-scroll-transition {
  z-index: 10;
  pointer-events: none;
  color: #cecece;
  position: absolute;
  inset: auto 0% 0%;
  width: 100%;
  height: 100%;
}

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

[data-arc-scroll-transition] svg {
  display: block;
  width: 100%;
  height: 100%;
}

[data-arc-scroll-transition] path {
  fill: currentColor;
}

@media screen and (max-width: 767px) {
  .arc-scroll__h {
    font-size: 3.5em;
    max-width: 10em;
  }
}
````

## 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 initArcScrollTransition() {
  const defaultMode = "cover";
  const defaultScrollStart = { cover: "bottom bottom", reveal: "top bottom" };
  const defaultScrollEnd = { cover: "bottom top", reveal: "top top" };
  const defaultScrub = 0.3;
  const defaultCurve = 12;
  const viewBox = 100;
  const svgNamespace = "http://www.w3.org/2000/svg";
  const instances = [];
  let mm = null;

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

  function getCurve(wrapper) {
    const value = parseFloat(wrapper.dataset.curve);
    return Number.isFinite(value) ? value : defaultCurve;
  }

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

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

  function round(value) {
    return Math.round(value * 100) / 100;
  }

  function buildShape(wrapper) {
    const shape = document.createElementNS(svgNamespace, "svg");
    shape.setAttribute("data-arc-scroll-shape", "");
    shape.setAttribute("viewBox", `0 0 ${viewBox} ${viewBox}`);
    shape.setAttribute("preserveAspectRatio", "none");
    shape.setAttribute("aria-hidden", "true");

    const path = document.createElementNS(svgNamespace, "path");
    path.setAttribute("data-arc-scroll-path", "");
    shape.appendChild(path);
    wrapper.appendChild(shape);

    return { shape, path };
  }

  function measureSection(instance) {
    const rect = instance.section.getBoundingClientRect();
    const aspect = rect.height ? rect.width / rect.height : 1;
    instance.depth = getCurve(instance.wrapper) * aspect;
  }

  function drawArc(instance, progress) {
    const fill = instance.mode === "cover" ? progress : 1 - progress;
    const curve = instance.depth * Math.sin(fill * Math.PI);

    if (instance.mode === "cover") {
      const edge = round(viewBox - viewBox * fill);
      const control = round(edge - curve * 2);
      instance.path.setAttribute(
        "d",
        `M0 ${viewBox} L0 ${edge} Q${viewBox / 2} ${control} ${viewBox} ${edge} L${viewBox} ${viewBox} Z`
      );
      return;
    }

    const edge = round(viewBox * fill);
    const control = round(edge + curve * 2);
    instance.path.setAttribute(
      "d",
      `M0 0 L0 ${edge} Q${viewBox / 2} ${control} ${viewBox} ${edge} L${viewBox} 0 Z`
    );
  }

  function createAnimation(instance) {
    const state = { progress: 0 };

    return gsap.to(state, {
      progress: 1,
      ease: "none",
      onUpdate: () => drawArc(instance, state.progress),
      scrollTrigger: {
        trigger: instance.section,
        start: getScrollStart(instance.wrapper, instance.mode),
        end: getScrollEnd(instance.wrapper, instance.mode),
        scrub: defaultScrub,
        invalidateOnRefresh: true,
        onRefresh: () => measureSection(instance),
      },
    });
  }

  function setupInstance(wrapper) {
    const { shape, path } = buildShape(wrapper);
    const instance = {
      wrapper,
      shape,
      path,
      section: wrapper.closest("section") || wrapper.parentElement,
      mode: getMode(wrapper),
      depth: 0,
    };

    measureSection(instance);
    drawArc(instance, 0);
    instance.tween = createAnimation(instance);
    return instance;
  }

  function destroyInstance(instance) {
    instance.tween.scrollTrigger?.kill();
    instance.tween.kill();
    instance.shape.remove();
  }

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

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

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

  mm = gsap.matchMedia();
  mm.add("(prefers-reduced-motion: no-preference)", () => {
    buildAll();
    return () => {
      destroyAll();
    };
  });
}

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