# Center Clip Wipe

> Sticky case studies that wipe open from a center clip with masked titles, parallax backgrounds, and a custom cursor — cinematic portfolio scrolling.

**Definition:** Center Clip Wipe 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-31
- Canonical page: https://html.bosconvertpro.com/collection/center-clip-wipe
- Full-page preview: https://html.bosconvertpro.com/collection/center-clip-wipe/full
- Live demo: https://html.bosconvertpro.com/demos/center-clip-wipe/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/center-clip-wipe/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Center Clip Wipe?

Center Clip Wipe is a scroll UI animation from collections.html. Sticky case studies that wipe open from a center clip with masked titles, parallax backgrounds, and a custom cursor — cinematic portfolio scrolling.

### What is Center Clip Wipe built with?

Center Clip Wipe 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 Center Clip Wipe in my project?

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

### Is Center Clip Wipe free to use?

Yes. The Center Clip Wipe 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>Center Clip Wipe</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:opsz,wght@14..32,400;14..32,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>
    <div class="cursor" data-cursor hidden><span>See case</span></div>

    <main class="works" data-works></main>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/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,
body {
  background: #0a0a0a;
  color: #fff;
  font-family: Inter, system-ui, sans-serif;
  font-optical-sizing: auto;
  letter-spacing: -0.04em;
  -webkit-font-smoothing: antialiased;
}

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

img {
  display: block;
}

a {
  color: inherit;
  text-decoration: none;
}

.works {
  position: relative;
  z-index: 1;
  width: 100%;
  background: #0a0a0a;
  cursor: none;
}

/* Sticky overlay: text + center card only */
.works__stage {
  position: sticky;
  top: 0;
  z-index: 2;
  width: 100%;
  height: 100vh;
  margin-bottom: -100vh;
  overflow: hidden;
  pointer-events: none;
}

.works__ui {
  position: absolute;
  inset: 0;
  z-index: 3;
}

.ui-layer__row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 40px;
  width: 100%;
  max-width: 1920px;
  height: 100%;
  padding: 0 20px;
}

.work__title,
.work__aside {
  display: flex;
  flex: 1 1 0;
  align-items: center;
  width: 1px;
  min-width: 0;
}

.work__title {
  font-size: 22px;
  font-weight: 400;
  line-height: 1.2;
  text-wrap: balance;
}

.work__mask {
  position: relative;
  overflow: hidden;
  width: 100%;
}

.work__mask h2,
.work__mask p {
  width: 100%;
  will-change: transform, filter, opacity;
}

.work__mask h2 + h2,
.work__mask p + p {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
}

.work__aside .work__mask {
  width: 70%;
}

.work__copy {
  font-size: 16px;
  font-weight: 400;
  line-height: 1.3;
  text-wrap: balance;
}

.work__slot {
  flex: 1 1 0;
  width: 1px;
  aspect-ratio: 4 / 3;
}

.card-stack {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 2;
  width: calc((min(100vw, 1920px) - 40px - 80px) / 3);
  max-width: 640px;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  transform: translate(-50%, -50%);
}

.card-layer {
  position: absolute;
  inset: 0;
  clip-path: inset(calc(var(--clip, 0) * 1%) 0 0 0);
  will-change: clip-path;
}

.card-layer img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
  pointer-events: none;
}

/* Backgrounds: normal page scroll */
.works__bgs {
  position: relative;
  z-index: 0;
}

.work-bg {
  position: relative;
  width: 100%;
  height: 110vh;
  overflow: hidden;
}

.work-bg__bleed {
  position: absolute;
  inset: 0;
  overflow: hidden;
  filter: brightness(0.6);
}

.work-bg__blur {
  position: absolute;
  inset: 0;
  z-index: 1;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  pointer-events: none;
}

.parallax {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.work-bg__bleed > .parallax {
  position: absolute;
  inset: 0;
}

.parallax__shift {
  position: absolute;
  inset: 0;
  will-change: transform;
}

.parallax__shift img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  pointer-events: none;
}

.cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 50;
  padding: 8px 14px;
  background: #fff;
  color: #0a0a0a;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}

.cursor[hidden] {
  display: none;
}

@media (max-width: 1199px) {
  .work__title {
    font-size: 20px;
  }

  .work__copy {
    font-size: 14px;
  }

  .card-stack {
    width: min(50vw, 520px);
    max-width: 50%;
  }
}

@media (max-width: 809px) {
  .works {
    cursor: auto;
  }

  .ui-layer__row {
    flex-direction: column;
    justify-content: flex-start;
    align-items: stretch;
    gap: 8px;
    padding: 24px 20px 0;
  }

  .work__title,
  .work__aside {
    flex: none;
    width: 100%;
    display: block;
  }

  .work__slot {
    display: none;
  }

  .work__title {
    font-size: 18px;
    line-height: 1.2;
    text-align: left;
    white-space: nowrap;
  }

  .work__mask h2 {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .work__aside .work__mask {
    width: 100%;
  }

  .work__copy {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    font-size: 14px;
    line-height: 1.35;
  }

  .card-stack {
    top: 50%;
    width: min(86vw, 420px);
    max-width: none;
    transform: translate(-50%, -50%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .card-layer {
    clip-path: none;
  }

  .parallax__shift {
    transform: none !important;
  }
}
````

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

const WORKS = [
  {
    title: "North House",
    copy: "A quiet architectural identity for homes built around light and landscape.",
    image:
      "https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&w=2000&q=80",
  },
  {
    title: "Atelier Desk",
    copy: "A workspace system for studios that still want the room to think.",
    image:
      "https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=2000&q=80",
  },
  {
    title: "Ridge",
    copy: "Editorial photography and a digital presence for alpine expeditions.",
    image:
      "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=2000&q=80",
  },
  {
    title: "Fernland",
    copy: "A conservation brand rooted in forest, water, and slow seasons.",
    image:
      "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=2000&q=80",
  },
  {
    title: "Civic",
    copy: "A contemporary identity for towers that meet the street with care.",
    image:
      "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=2000&q=80",
  },
  {
    title: "Harbor Night",
    copy: "A city guide rebuilt as a cinematic, after-hours reading experience.",
    image:
      "https://images.unsplash.com/photo-1514565131-fce0801e5785?auto=format&fit=crop&w=2000&q=80",
  },
  {
    title: "Grain",
    copy: "Material-led furniture stories for rooms that feel lived in.",
    image:
      "https://images.unsplash.com/photo-1519710164239-da123dc03ef4?auto=format&fit=crop&w=2000&q=80",
  },
  {
    title: "Solace",
    copy: "An abstract visual language for a gallery of quiet, slow work.",
    image:
      "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=2000&q=80",
  },
];

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function setClip(el, clip) {
  if (clip <= 0) {
    el.style.setProperty("--clip", "0");
    el.style.visibility = "";
    return;
  }
  if (clip >= 100) {
    el.style.setProperty("--clip", "101");
    el.style.visibility = "hidden";
    return;
  }
  el.style.visibility = "";
  el.style.setProperty("--clip", String(clip));
}

function parallaxMarkup(src, alt) {
  return `
    <div class="parallax" data-parallax="40" data-focal-x="50" data-focal-y="50">
      <div class="parallax__shift">
        <img src="${src}" alt="${alt}" draggable="false" />
      </div>
    </div>
  `;
}

function renderWorks() {
  const root = document.querySelector("[data-works]");
  const first = WORKS[0];

  const cards = WORKS.map(
    (work, i) => `
      <div class="card-layer" data-card-layer style="z-index:${i}; --clip:${i === 0 ? 0 : 100}">
        <img src="${work.image}" alt="${work.title}" draggable="false" />
      </div>
    `
  ).join("");

  const bgs = WORKS.map(
    (work) => `
      <section class="work-bg" data-work-bg>
        <div class="work-bg__bleed">
          <div class="work-bg__blur"></div>
          ${parallaxMarkup(work.image, "")}
        </div>
      </section>
    `
  ).join("");

  root.innerHTML = `
    <div class="works__stage">
      <div class="works__ui">
        <div class="ui-layer__row">
          <div class="work__title">
            <div class="work__mask" data-title-mask>
              <h2 data-title-text>${first.title}</h2>
            </div>
          </div>
          <div class="work__slot" aria-hidden="true"></div>
          <div class="work__aside">
            <div class="work__mask" data-copy-mask>
              <p class="work__copy" data-copy-text>${first.copy}</p>
            </div>
          </div>
        </div>
      </div>
      <div class="card-stack">${cards}</div>
    </div>
    <div class="works__bgs">${bgs}</div>
  `;
}

function parallaxMetrics(node, percent) {
  const size = node.offsetHeight || 1;
  const extra = clamp(size * (clamp(percent, 5, 60) / 100), 40, 480);
  return {
    scale: 1 + extra / size,
    from: -extra / 2,
    to: extra / 2,
  };
}

function bindParallax(node, trigger, start, end) {
  const shift = node.querySelector(".parallax__shift");
  if (!shift) return;

  const percent = Number(node.dataset.parallax) || 40;
  const focalX = Number(node.dataset.focalX) || 50;
  const focalY = Number(node.dataset.focalY) || 50;

  gsap.set(shift, { transformOrigin: `${focalX}% ${focalY}%` });

  gsap.fromTo(
    shift,
    {
      y: () => parallaxMetrics(node, percent).from,
      scale: () => parallaxMetrics(node, percent).scale,
    },
    {
      y: () => parallaxMetrics(node, percent).to,
      scale: () => parallaxMetrics(node, percent).scale,
      ease: "none",
      scrollTrigger: {
        trigger,
        start,
        end,
        scrub: true,
        invalidateOnRefresh: true,
      },
    }
  );
}

function setCardLayers(cardLayers, visibleThrough, wipingIndex, wipeT) {
  cardLayers.forEach((layer, i) => {
    if (i <= visibleThrough) setClip(layer, 0);
    else if (i === wipingIndex) setClip(layer, 100 * (1 - wipeT));
    else setClip(layer, 100);
  });
}

function swapMaskedText(mask, incomingHtml, direction) {
  const outgoing = Array.from(mask.children);
  if (!outgoing.length) return;

  const next = outgoing[0].cloneNode(false);
  next.textContent = incomingHtml;
  gsap.set(next, {
    yPercent: direction > 0 ? 40 : -40,
    autoAlpha: 0,
    filter: "blur(12px)",
  });

  const outCount = outgoing.length;
  let done = 0;

  function playIn() {
    mask.appendChild(next);
    gsap.to(next, {
      yPercent: 0,
      autoAlpha: 1,
      filter: "blur(0px)",
      duration: 0.65,
      ease: "power3.out",
      overwrite: true,
      onComplete: () => {
        gsap.set(next, {
          yPercent: 0,
          y: 0,
          autoAlpha: 1,
          filter: "blur(0px)",
          clearProps: "transform,filter",
        });
      },
    });
  }

  outgoing.forEach((el) => {
    gsap.killTweensOf(el);
    gsap.to(el, {
      yPercent: direction > 0 ? -40 : 40,
      autoAlpha: 0,
      filter: "blur(12px)",
      duration: 0.4,
      ease: "power2.in",
      overwrite: true,
      onComplete: () => {
        el.remove();
        done += 1;
        if (done === outCount) playIn();
      },
    });
  });
}

function initOverlayWipe(root) {
  const cardStack = root.querySelector(".card-stack");
  const cardLayers = Array.from(root.querySelectorAll("[data-card-layer]"));
  const bgs = Array.from(root.querySelectorAll("[data-work-bg]"));
  const titleMask = root.querySelector("[data-title-mask]");
  const copyMask = root.querySelector("[data-copy-mask]");
  if (!cardStack || cardLayers.length < 2 || bgs.length < 2) return;

  let activeIndex = 0;
  setCardLayers(cardLayers, 0, -1, 0);

  function cardEdges() {
    const rect = cardStack.getBoundingClientRect();
    return { top: rect.top, bottom: rect.bottom };
  }

  function goToText(index, direction) {
    if (index === activeIndex) return;
    if (index < 0 || index >= WORKS.length) return;
    activeIndex = index;
    const work = WORKS[index];
    swapMaskedText(titleMask, work.title, direction);
    swapMaskedText(copyMask, work.copy, direction);
  }

  bgs.forEach((bg, i) => {
    if (i === 0) return;

    ScrollTrigger.create({
      trigger: bg,
      // Split line = this section's top.
      // Start when it meets the center card bottom.
      // End when it meets the center card top.
      start: () => `top ${cardEdges().bottom}px`,
      end: () => `top ${cardEdges().top}px`,
      scrub: true,
      invalidateOnRefresh: true,
      onUpdate: (self) => {
        setCardLayers(cardLayers, i - 1, i, self.progress);

        const nextIndex = self.progress >= 0.5 ? i : i - 1;
        if (nextIndex !== activeIndex) {
          goToText(nextIndex, nextIndex > activeIndex ? 1 : -1);
        }
      },
    });
  });
}

function initParallax(root) {
  root.querySelectorAll("[data-work-bg] .parallax").forEach((node) => {
    bindParallax(node, node.closest("[data-work-bg]"), "top bottom", "bottom top");
  });
}

function initSmoothScroll() {
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
  if (typeof Lenis === "undefined") return;

  const lenis = new Lenis({
    duration: 1.15,
    smoothWheel: true,
    touchMultiplier: 1.1,
  });

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

function initCursor() {
  const cursor = document.querySelector("[data-cursor]");
  const root = document.querySelector("[data-works]");
  const canHover = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
  if (!cursor || !root || !canHover) return;

  gsap.set(cursor, { xPercent: -50, yPercent: -50 });
  const xTo = gsap.quickTo(cursor, "x", { duration: 0.18, ease: "power3.out" });
  const yTo = gsap.quickTo(cursor, "y", { duration: 0.18, ease: "power3.out" });

  window.addEventListener(
    "pointermove",
    (event) => {
      xTo(event.clientX);
      yTo(event.clientY);
    },
    { passive: true }
  );

  root.addEventListener("pointerenter", () => {
    cursor.hidden = false;
    gsap.fromTo(
      cursor,
      { opacity: 0, scale: 0.92 },
      { opacity: 1, scale: 1, duration: 0.2, overwrite: "auto" }
    );
  });
  root.addEventListener("pointerleave", () => {
    gsap.to(cursor, {
      opacity: 0,
      duration: 0.16,
      onComplete: () => {
        cursor.hidden = true;
      },
    });
  });
}

document.addEventListener("DOMContentLoaded", () => {
  renderWorks();
  initSmoothScroll();
  const root = document.querySelector("[data-works]");
  initParallax(root);
  initOverlayWipe(root);
  initCursor();
  ScrollTrigger.refresh();
});
````
