# Wipe Slider

> Full-bleed slides that transition with directional wipe masks — cinematic carousel motion for hero galleries.

**Definition:** Wipe Slider is a sliders 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: Sliders
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-03
- Canonical page: https://html.bosconvertpro.com/collection/wipe-slider
- Full-page preview: https://html.bosconvertpro.com/collection/wipe-slider/full
- Live demo: https://html.bosconvertpro.com/demos/wipe-slider/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/wipe-slider/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Wipe Slider?

Wipe Slider is a sliders UI animation from collections.html. Full-bleed slides that transition with directional wipe masks — cinematic carousel motion for hero galleries.

### What is Wipe Slider built with?

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

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

### Is Wipe Slider free to use?

Yes. The Wipe Slider 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>Wipe Slider</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 rel="preconnect" href="https://wsrv.nl" />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <section
      class="wipe-slider"
      data-wipe-slider
      data-duration="1.1"
      data-ease="expo.inOut"
      data-stagger="0.00875"
      data-start-index="0"
    >
      <div class="wipe-slider__data" data-wipe-slider-data hidden>
        <div
          data-wipe-slide
          data-title="Dawn"
          data-label="01 · Coast"
          data-desc="The shoreline is still empty when the first mile starts. Salt air, cold light, and a body that has not fully agreed yet."
          data-bg="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1507525428034-b723cf961d3e&w=1920&h=1280&fit=cover&q=82&output=webp"
        ></div>
        <div
          data-wipe-slide
          data-title="Trail"
          data-label="02 · Forest"
          data-desc="Trees close in and the path narrows. Each step is quieter than the last — the only sound left is breath and grit underfoot."
          data-bg="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1441974231531-c6227db76b6e&w=1920&h=1280&fit=cover&q=82&output=webp"
        ></div>
        <div
          data-wipe-slide
          data-title="Summit"
          data-label="03 · Alpine"
          data-desc="Altitude rewrites the rules. Thin air, wide sky, and a final ridge that only opens after you stop looking for shortcuts."
          data-bg="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1506905925346-21bda4d32df4&w=1920&h=1280&fit=cover&q=82&output=webp"
        ></div>
        <div
          data-wipe-slide
          data-title="Pulse"
          data-label="04 · City"
          data-desc="Neon, asphalt, and a tempo that does not slow. The finish line is not a place — it is the next block you decide to take."
          data-bg="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1514565131-fce0801e5785&w=1920&h=1280&fit=cover&q=82&output=webp"
        ></div>
        <div
          data-wipe-slide
          data-title="Horizon"
          data-label="05 · Desert"
          data-desc="Heat settles into the sand and the road thins to a line. Out here the only clock is the sun, and it never hurries for anyone."
          data-bg="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1509316785289-025f5b846b35&w=1920&h=1280&fit=cover&q=82&output=webp"
        ></div>
      </div>

      <div class="wipe-slider__stage" data-wipe-slider-stage></div>
    </section>

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

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

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #101010;
}

.wipe-slider {
  --wipe-slider-bg: #101010;
  --wipe-slider-fg: rgba(255, 250, 242, 0.92);
  --wipe-slider-fg-soft: rgba(255, 255, 255, 0.85);
  --wipe-slider-fg-dim: rgba(255, 255, 255, 0.75);
  --wipe-slider-line: rgba(255, 255, 255, 0.25);
  --wipe-slider-line-on: rgba(255, 255, 255, 0.9);
  --wipe-slider-pill-fill: linear-gradient(
    145deg,
    rgba(255, 255, 255, 0.28) 0%,
    rgba(255, 255, 255, 0.08) 42%,
    rgba(255, 255, 255, 0.04) 100%
  );
  --wipe-slider-pill-border: rgba(255, 255, 255, 0.28);
  --wipe-slider-pill-shine: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.55) 0%,
    rgba(255, 255, 255, 0.12) 38%,
    transparent 68%
  );
  --wipe-slider-circle: linear-gradient(
    160deg,
    rgba(255, 255, 255, 0.32) 0%,
    rgba(255, 255, 255, 0.08) 55%,
    rgba(255, 255, 255, 0.04) 100%
  );
  --wipe-slider-circle-hover: linear-gradient(
    160deg,
    rgba(255, 255, 255, 0.42) 0%,
    rgba(255, 255, 255, 0.16) 55%,
    rgba(255, 255, 255, 0.08) 100%
  );
  --wipe-slider-margin: 40px;
  --wipe-slider-font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;

  position: relative;
  width: 100%;
  height: 100dvh;
  overflow: hidden;
  background: var(--wipe-slider-bg);
  font-family: var(--wipe-slider-font);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  touch-action: pan-y;
  outline: none;
}

.wipe-slider__data {
  display: none;
}

.wipe-slider__stage {
  position: relative;
  width: 100%;
  height: 100%;
}

.wipe-slider__media {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.wipe-slider__layer {
  position: absolute;
  inset: 0;
  will-change: clip-path;
}

.wipe-slider__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  transform-origin: 50% 50%;
  filter: brightness(0.9);
  will-change: transform;
  user-select: none;
  -webkit-user-drag: none;
}

.wipe-slider__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(10, 10, 10, 0.55) 0%,
    rgba(10, 10, 10, 0.05) 45%,
    rgba(10, 10, 10, 0.25) 100%
  );
}

.wipe-slider__title {
  position: absolute;
  left: var(--wipe-slider-margin);
  top: var(--wipe-slider-margin);
  z-index: 4;
  margin: 0;
  font-size: clamp(64px, 11vw, 176px);
  font-weight: 320;
  letter-spacing: -0.02em;
  line-height: 0.95;
  color: var(--wipe-slider-fg);
  width: max-content;
  pointer-events: auto;
  user-select: text;
}

.wipe-slider__title-line {
  position: relative;
  display: block;
  white-space: nowrap;
  width: max-content;
  overflow-y: hidden;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
  padding-inline: 0.06em;
  margin-inline: -0.06em;
}

.wipe-slider__title-char {
  position: relative;
  display: inline-block;
  will-change: transform;
}

.wipe-slider__info {
  position: absolute;
  left: var(--wipe-slider-margin);
  right: var(--wipe-slider-margin);
  top: 47%;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  z-index: 4;
  pointer-events: auto;
  user-select: text;
}

.wipe-slider__label {
  position: relative;
  flex: 0 0 auto;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--wipe-slider-fg-soft);
}

.wipe-slider__label-line {
  position: relative;
  display: block;
  white-space: nowrap;
  width: max-content;
  overflow: hidden;
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
}

.wipe-slider__desc-slot {
  position: relative;
  flex: 0 0 340px;
  width: 340px;
  max-width: 40vw;
  margin-left: 18vw;
}

.wipe-slider__desc {
  width: 100%;
  margin: 0;
  font-size: 15px;
  font-weight: 380;
  line-height: 1.55;
  color: var(--wipe-slider-fg-soft);
}

.wipe-slider__desc-clone {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  margin: 0;
}

.wipe-slider__desc-line {
  position: relative;
  will-change: transform, opacity;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.wipe-slider__percent {
  flex: 0 0 auto;
  display: flex;
  align-items: flex-start;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--wipe-slider-fg-dim);
}

.wipe-slider__digit {
  position: relative;
  display: inline-block;
  overflow-x: visible;
  overflow-y: hidden;
  height: 1em;
  width: 0.6em;
  vertical-align: top;
}

.wipe-slider__digit-roller {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  flex-direction: column;
  will-change: transform;
}

.wipe-slider__digit-roller span {
  display: block;
  height: 1em;
  line-height: 1;
}

.wipe-slider__percent-sign {
  display: block;
  height: 1em;
  line-height: 1;
  margin-left: 0.05em;
}

.wipe-slider__pill {
  position: absolute;
  left: 50%;
  bottom: 68px;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 6px;
  border-radius: 999px;
  isolation: isolate;
  overflow: hidden;
  transform: translateX(-50%);
  background: var(--wipe-slider-pill-fill);
  border: 1px solid var(--wipe-slider-pill-border);
  -webkit-backdrop-filter: blur(28px) saturate(180%) brightness(1.08);
  backdrop-filter: blur(28px) saturate(180%) brightness(1.08);
  box-shadow:
    0 10px 40px rgba(0, 0, 0, 0.28),
    0 2px 8px rgba(0, 0, 0, 0.12),
    inset 0 1px 0 rgba(255, 255, 255, 0.45),
    inset 0 -1px 0 rgba(255, 255, 255, 0.08),
    inset 1px 0 0 rgba(255, 255, 255, 0.12),
    inset -1px 0 0 rgba(255, 255, 255, 0.06);
}

.wipe-slider__pill::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--wipe-slider-pill-shine);
  opacity: 0.85;
  pointer-events: none;
  z-index: 0;
}

.wipe-slider__pill::after {
  content: "";
  position: absolute;
  top: -40%;
  left: 8%;
  width: 55%;
  height: 90%;
  border-radius: 50%;
  background: radial-gradient(
    ellipse at 40% 30%,
    rgba(255, 255, 255, 0.38) 0%,
    rgba(255, 255, 255, 0.1) 42%,
    transparent 72%
  );
  filter: blur(6px);
  pointer-events: none;
  z-index: 0;
  animation: wipe-slider-liquid-drift 7s ease-in-out infinite alternate;
}

@keyframes wipe-slider-liquid-drift {
  0% {
    transform: translate3d(0, 0, 0) scale(1);
    opacity: 0.75;
  }
  100% {
    transform: translate3d(18%, 8%, 0) scale(1.08);
    opacity: 1;
  }
}

.wipe-slider__pill > * {
  position: relative;
  z-index: 1;
}

.wipe-slider__arrow {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: var(--wipe-slider-circle);
  color: var(--wipe-slider-fg);
  cursor: pointer;
  overflow: hidden;
  isolation: isolate;
  -webkit-backdrop-filter: blur(12px) saturate(160%);
  backdrop-filter: blur(12px) saturate(160%);
  box-shadow:
    0 4px 14px rgba(0, 0, 0, 0.18),
    inset 0 1px 0 rgba(255, 255, 255, 0.4),
    inset 0 -1px 0 rgba(255, 255, 255, 0.06);
  transition:
    background 0.3s ease,
    border-color 0.3s ease,
    box-shadow 0.3s ease;
}

.wipe-slider__arrow::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(
    155deg,
    rgba(255, 255, 255, 0.4) 0%,
    rgba(255, 255, 255, 0.08) 40%,
    transparent 65%
  );
  pointer-events: none;
  z-index: 0;
}

.wipe-slider__arrow:hover {
  background: var(--wipe-slider-circle-hover);
  border-color: rgba(255, 255, 255, 0.38);
  box-shadow:
    0 6px 18px rgba(0, 0, 0, 0.22),
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 -1px 0 rgba(255, 255, 255, 0.08);
}

.wipe-slider__arrow:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.8);
  outline-offset: 2px;
}

.wipe-slider__arrow-icon {
  position: relative;
  z-index: 1;
  display: block;
  width: 16px;
  height: 16px;
  will-change: transform;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.25));
}

.wipe-slider__pill-label {
  position: relative;
  box-sizing: content-box;
  height: 20px;
  line-height: 20px;
  padding: 0 8px;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--wipe-slider-fg);
  text-align: center;
  overflow: hidden;
}

.wipe-slider__pill-label-line {
  position: relative;
  display: block;
  white-space: nowrap;
  width: max-content;
  margin-left: auto;
  margin-right: auto;
  overflow: hidden;
  padding-bottom: 0.06em;
  margin-bottom: -0.06em;
}

.wipe-slider__pill-char {
  position: relative;
  display: inline-block;
  will-change: transform, opacity;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transform: translateZ(0);
}

.wipe-slider__line-out {
  position: absolute;
  top: 0;
  left: 0;
  width: max-content;
  min-width: 100%;
}

.wipe-slider__pill-label .wipe-slider__line-out {
  left: 50%;
  transform: translateX(-50%);
  min-width: 0;
}

.wipe-slider__zone {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 25%;
  z-index: 3;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.wipe-slider__zone--prev {
  left: 0;
}

.wipe-slider__zone--next {
  right: 0;
}

.wipe-slider__bars {
  position: absolute;
  left: var(--wipe-slider-margin);
  right: var(--wipe-slider-margin);
  bottom: 24px;
  z-index: 4;
  display: grid;
  grid-template-columns: repeat(var(--wipe-slider-count, 4), 1fr);
  gap: 24px;
}

.wipe-slider__bar {
  position: relative;
  height: 2px;
  background: var(--wipe-slider-line);
  overflow: hidden;
}

.wipe-slider__bar-fill {
  position: absolute;
  inset: 0;
  background: var(--wipe-slider-line-on);
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left center;
  will-change: transform;
}

@media (max-width: 767px) {
  .wipe-slider {
    --wipe-slider-margin: 20px;
  }

  .wipe-slider__title {
    font-size: clamp(44px, 15vw, 72px);
  }

  .wipe-slider__info {
    top: auto;
    bottom: 128px;
    flex-direction: column;
    gap: 12px;
  }

  .wipe-slider__desc-slot {
    margin-left: 0;
    max-width: 100%;
    flex-basis: auto;
    width: 100%;
  }

  .wipe-slider__percent {
    display: none;
  }

  .wipe-slider__pill {
    bottom: 58px;
  }

  .wipe-slider__bars {
    gap: 8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .wipe-slider__pill::after {
    animation: none;
  }
}
````

## Source — script.js
````javascript
(() => {
  "use strict";

  function whenReady(fn, attempt) {
    attempt = attempt || 0;
    if (typeof gsap === "undefined") {
      if (attempt < 100) setTimeout(() => whenReady(fn, attempt + 1), 50);
      else console.error("[Wipe Slider] GSAP failed to load.");
      return;
    }
    fn();
  }

  whenReady(() => {
    const plugins = [];
    if (typeof CustomEase !== "undefined") plugins.push(CustomEase);
    if (typeof SplitText !== "undefined") plugins.push(SplitText);
    if (plugins.length) gsap.registerPlugin.apply(gsap, plugins);

    if (typeof CustomEase !== "undefined") {
      CustomEase.create("wipeTextIn", "M0,0 C0.08,0.82 0.16,1 1,1");
      CustomEase.create("wipeTextOut", "M0,0 C0.55,0 0.78,0.2 1,1");
      CustomEase.create("wipeTextSoft", "M0,0 C0.22,1 0.36,1 1,1");
    }

    const instances = [];

    document.querySelectorAll("[data-wipe-slider]").forEach((root) => {
      const api = initWipeSlider(root);
      if (api) instances.push(api);
    });

    window.WipeSlider = {
      refresh() {
        instances.forEach((i) => i.refresh());
      },
      next() {
        instances.forEach((i) => i.next());
      },
      prev() {
        instances.forEach((i) => i.prev());
      },
      goTo(index) {
        instances.forEach((i) => i.goTo(index));
      },
    };
  });

  function initWipeSlider(root) {
    const dataHost = root.querySelector("[data-wipe-slider-data]");
    const stage = root.querySelector("[data-wipe-slider-stage]");
    const slideNodes = dataHost
      ? Array.from(dataHost.querySelectorAll("[data-wipe-slide]"))
      : [];

    if (!stage || !slideNodes.length) return null;

    const duration = parseFloat(root.dataset.duration || "1.1");
    const ease = root.dataset.ease || "expo.inOut";
    const charStagger = parseFloat(root.dataset.stagger || "0.00875");
    const startIndex = parseInt(root.dataset.startIndex || "0", 10);

    const hasCustomEase = typeof CustomEase !== "undefined";
    const TEXT_IN = hasCustomEase ? "wipeTextIn" : "power4.out";
    const TEXT_OUT = hasCustomEase ? "wipeTextOut" : "power3.in";
    const TEXT_SOFT = hasCustomEase ? "wipeTextSoft" : "power3.out";

    const LABEL_OUT_DUR = 0.48;
    const LABEL_IN_DUR = 0.85;
    const LABEL_IN_DELAY = 0.1;
    const PILL_OUT_DUR = 0.45;
    const PILL_IN_DUR = 0.8;
    const PILL_IN_DELAY = 0.08;
    const PILL_CHAR_STAGGER = 0.016;
    const DESC_OUT_DUR = 0.52;
    const DESC_IN_DUR = 1.0;
    const DESC_IN_DELAY = 0.12;
    const DESC_STAGGER = 0.055;
    const LINE_TRAVEL = 105;

    const WIPE_DURATION = 1.15 * duration;
    const BAR_DURATION = 0.85 * duration;
    const PRESS_DURATION = 0.15;
    const SWIPE_THRESHOLD = 40;
    const NAV_THROTTLE_MS = 150;

    const slides = slideNodes.map((node) => ({
      title: node.dataset.title || "",
      label: node.dataset.label || "",
      desc: node.dataset.desc || "",
      bg: node.dataset.bg || "",
    }));

    let current = Math.min(Math.max(startIndex, 0), slides.length - 1);
    let descSplit = null;
    let pointerDown = false;
    let pointerStartX = 0;
    let lastNavAt = 0;
    let activeTransition = null;
    let zBump = 0;
    let wipeSeq = 0;
    let ui = null;

    root.style.setProperty("--wipe-slider-count", String(slides.length));

    function percentFor(index) {
      return String(Math.round(((index + 1) / slides.length) * 100)).padStart(
        3,
        "0"
      );
    }

    function setWipeClip(layer, side, lead, trail) {
      if (trail === undefined) trail = lead;

      if (lead <= 0 && trail <= 0) {
        layer.style.clipPath = "none";
        layer.style.visibility = "";
        return;
      }

      if (lead >= 100 && trail >= 100) {
        layer.style.clipPath = "none";
        layer.style.visibility = "hidden";
        return;
      }

      layer.style.visibility = "";

      if (side === "l") {
        layer.style.clipPath =
          "polygon(" +
          lead +
          "% 0%, 100% 0%, 100% 100%, " +
          trail +
          "% 100%)";
      } else {
        layer.style.clipPath =
          "polygon(0% 0%, " +
          (100 - lead) +
          "% 0%, " +
          (100 - trail) +
          "% 100%, 0% 100%)";
      }
    }

    function createCharLine(text, lineClass, charClass) {
      const line = document.createElement("span");
      line.className = lineClass;

      text.split("").forEach((ch) => {
        const char = document.createElement("span");
        char.className = charClass;
        char.textContent = ch === " " ? "\u00A0" : ch;
        line.appendChild(char);
      });

      return line;
    }

    function setCharLine(host, text, lineClass, charClass) {
      host.innerHTML = "";
      const line = createCharLine(text, lineClass, charClass);
      host.appendChild(line);
      return Array.from(line.querySelectorAll("." + charClass));
    }

    function swapCharLine(host, text, lineClass, charClass, outVars, inVars) {
      const existing = host.querySelector(
        "." + lineClass + ":not(.wipe-slider__line-out)"
      );

      if (existing) {
        existing.classList.add("wipe-slider__line-out");
        const outChars = existing.querySelectorAll("." + charClass);
        gsap.to(
          outChars,
          Object.assign(
            {
              onComplete() {
                existing.remove();
              },
            },
            outVars
          )
        );
      }

      const line = createCharLine(text, lineClass, charClass);
      host.appendChild(line);
      const inChars = Array.from(line.querySelectorAll("." + charClass));
      gsap.set(inChars, inVars.from);
      gsap.to(inChars, inVars.to);
      return line;
    }

    function buildDigit(digitChar) {
      const wrap = document.createElement("span");
      wrap.className = "wipe-slider__digit";

      const roller = document.createElement("span");
      roller.className = "wipe-slider__digit-roller";

      for (let d = 0; d <= 9; d++) {
        const s = document.createElement("span");
        s.textContent = String(d);
        roller.appendChild(s);
      }

      wrap.appendChild(roller);
      const value = parseInt(digitChar, 10);
      wrap._digit = value;
      wrap._roller = roller;
      gsap.set(roller, { y: -value + "em" });
      return wrap;
    }

    function renderPercent(host, valueStr) {
      host.innerHTML = "";
      String(valueStr)
        .split("")
        .forEach((ch) => {
          host.appendChild(buildDigit(ch));
        });

      const sign = document.createElement("span");
      sign.className = "wipe-slider__percent-sign";
      sign.textContent = "%";
      host.appendChild(sign);
    }

    function rollPercent(host, valueStr) {
      const digits = String(valueStr).split("");
      let wraps = Array.from(host.querySelectorAll(".wipe-slider__digit"));

      if (wraps.length !== digits.length) {
        const prev = wraps.map((w) => w._digit || 0);
        const pad = digits.length - prev.length;
        const seed = digits
          .map((_, i) => {
            const src = i - pad;
            return src >= 0 && src < prev.length ? String(prev[src]) : "0";
          })
          .join("");
        renderPercent(host, seed);
        wraps = Array.from(host.querySelectorAll(".wipe-slider__digit"));
      }

      wraps.forEach((wrap, i) => {
        const next = parseInt(digits[i], 10);
        wrap._digit = next;
        gsap.to(wrap._roller, {
          y: -next + "em",
          duration: 1.05,
          ease: TEXT_IN,
          delay: i * 0.04,
          force3D: true,
          overwrite: true,
        });
      });
    }

    function measurePillWidth(lineEl) {
      return Math.max(24, lineEl.scrollWidth);
    }

    function splitDescription(text) {
      if (descSplit) {
        descSplit.revert();
        descSplit = null;
      }

      ui.descEl.textContent = text;

      if (typeof SplitText === "undefined" || !SplitText.create) {
        return [ui.descEl];
      }

      descSplit = SplitText.create(ui.descEl, {
        type: "lines",
        mask: "lines",
        linesClass: "wipe-slider__desc-line",
      });

      return descSplit.lines;
    }

    function bindArrowMotion(btn, direction) {
      const icon = btn.querySelector(".wipe-slider__arrow-icon");

      btn.addEventListener("mouseenter", () => {
        gsap.to(icon, {
          x: 3 * direction,
          duration: 0.35,
          ease: "back.out(2)",
          overwrite: "auto",
          force3D: true,
        });
      });

      btn.addEventListener("mouseleave", () => {
        gsap.to(icon, {
          x: 0,
          duration: 0.35,
          ease: "expo.out",
          overwrite: "auto",
          force3D: true,
        });
      });

      btn.addEventListener("pointerdown", () => {
        gsap.to(btn, {
          scale: 0.92,
          duration: PRESS_DURATION,
          ease: "power2.out",
          overwrite: "auto",
          force3D: true,
        });
      });

      btn.addEventListener("pointerup", () => {
        gsap.to(btn, {
          scale: 1,
          duration: PRESS_DURATION,
          ease: "back.out(2.5)",
          overwrite: "auto",
          force3D: true,
        });
      });

      btn.addEventListener("pointerleave", () => {
        gsap.to(btn, {
          scale: 1,
          duration: PRESS_DURATION,
          ease: "power2.out",
          overwrite: "auto",
          force3D: true,
        });
      });
    }

    function buildStage() {
      stage.innerHTML = "";

      const media = document.createElement("div");
      media.className = "wipe-slider__media";

      const bgLayers = slides.map((slide, i) => {
        const layer = document.createElement("div");
        layer.className = "wipe-slider__layer";
        layer.style.zIndex = String(i);
        setWipeClip(layer, "l", i === current ? 0 : 100);

        const img = document.createElement("img");
        img.className = "wipe-slider__image";
        img.src = slide.bg;
        img.alt = "";
        img.setAttribute("aria-hidden", "true");
        img.draggable = false;
        img.loading = i === current ? "eager" : "lazy";

        layer.appendChild(img);
        media.appendChild(layer);
        return layer;
      });

      const scrim = document.createElement("div");
      scrim.className = "wipe-slider__scrim";
      scrim.setAttribute("aria-hidden", "true");

      const titleEl = document.createElement("h1");
      titleEl.className = "wipe-slider__title";

      const info = document.createElement("div");
      info.className = "wipe-slider__info";

      const labelEl = document.createElement("div");
      labelEl.className = "wipe-slider__label";

      const descSlot = document.createElement("div");
      descSlot.className = "wipe-slider__desc-slot";

      const descEl = document.createElement("p");
      descEl.className = "wipe-slider__desc";
      descSlot.appendChild(descEl);

      const percentEl = document.createElement("div");
      percentEl.className = "wipe-slider__percent";

      info.appendChild(labelEl);
      info.appendChild(descSlot);
      info.appendChild(percentEl);

      const pill = document.createElement("div");
      pill.className = "wipe-slider__pill";
      pill.setAttribute("role", "group");
      pill.setAttribute("aria-label", "Slide controls");

      const prevBtn = document.createElement("button");
      prevBtn.type = "button";
      prevBtn.className = "wipe-slider__arrow";
      prevBtn.setAttribute("aria-label", "Previous slide");
      prevBtn.innerHTML =
        '<svg class="wipe-slider__arrow-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M10 3L5 8L10 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';

      const pillLabel = document.createElement("div");
      pillLabel.className = "wipe-slider__pill-label";

      const nextBtn = document.createElement("button");
      nextBtn.type = "button";
      nextBtn.className = "wipe-slider__arrow";
      nextBtn.setAttribute("aria-label", "Next slide");
      nextBtn.innerHTML =
        '<svg class="wipe-slider__arrow-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';

      pill.appendChild(prevBtn);
      pill.appendChild(pillLabel);
      pill.appendChild(nextBtn);

      const bars = document.createElement("div");
      bars.className = "wipe-slider__bars";
      bars.setAttribute("aria-hidden", "true");

      const barEls = slides.map(() => {
        const bar = document.createElement("div");
        bar.className = "wipe-slider__bar";
        const fill = document.createElement("div");
        fill.className = "wipe-slider__bar-fill";
        bar.appendChild(fill);
        bars.appendChild(bar);
        return { bar, fill };
      });

      const zonePrev = document.createElement("div");
      zonePrev.className = "wipe-slider__zone wipe-slider__zone--prev";
      zonePrev.setAttribute("aria-hidden", "true");

      const zoneNext = document.createElement("div");
      zoneNext.className = "wipe-slider__zone wipe-slider__zone--next";
      zoneNext.setAttribute("aria-hidden", "true");

      stage.appendChild(media);
      stage.appendChild(scrim);
      stage.appendChild(titleEl);
      stage.appendChild(info);
      stage.appendChild(zonePrev);
      stage.appendChild(zoneNext);
      stage.appendChild(pill);
      stage.appendChild(bars);

      return {
        bgLayers,
        titleEl,
        labelEl,
        descSlot,
        descEl,
        percentEl,
        pillLabel,
        prevBtn,
        nextBtn,
        barEls,
        zonePrev,
        zoneNext,
      };
    }

    function paintInitial() {
      const slide = slides[current];

      const titleChars = setCharLine(
        ui.titleEl,
        slide.title,
        "wipe-slider__title-line",
        "wipe-slider__title-char"
      );
      gsap.set(titleChars, { yPercent: 0, y: 0 });

      const labelChars = setCharLine(
        ui.labelEl,
        slide.label,
        "wipe-slider__label-line",
        "wipe-slider__title-char"
      );
      gsap.set(labelChars, { yPercent: 0, y: 0, opacity: 1 });

      const descLines = splitDescription(slide.desc);
      gsap.set(descLines, { yPercent: 0, y: 0, opacity: 1 });

      renderPercent(ui.percentEl, percentFor(current));

      const pillChars = setCharLine(
        ui.pillLabel,
        slide.title,
        "wipe-slider__pill-label-line",
        "wipe-slider__pill-char"
      );
      gsap.set(pillChars, { yPercent: 0, y: 0, opacity: 1 });

      const pillLine = ui.pillLabel.querySelector(
        ".wipe-slider__pill-label-line:not(.wipe-slider__line-out)"
      );
      if (pillLine) {
        gsap.set(ui.pillLabel, { width: measurePillWidth(pillLine) });
      }

      ui.barEls.forEach((item, i) => {
        gsap.set(item.fill, {
          scaleX: i <= current ? 1 : 0,
          opacity: 1,
          transformOrigin: "left center",
        });
      });
    }

    function goTo(index, direction) {
      if (index === current) return;

      const now = performance.now();
      if (now - lastNavAt < NAV_THROTTLE_MS) return;
      lastNavAt = now;

      if (activeTransition && index === activeTransition.outIndex) {
        activeTransition.tl.progress(1);
        activeTransition = null;
      }

      const previous = current;
      current = index;
      const slide = slides[index];
      root.setAttribute("aria-label", slide.title);

      const seq = ++wipeSeq;
      const tl = gsap.timeline({
        onComplete() {
          if (activeTransition && activeTransition.tl === tl) {
            activeTransition = null;
          }
        },
      });
      activeTransition = { tl, outIndex: previous };

      const incoming = ui.bgLayers[index];
      zBump += 1;
      incoming.style.zIndex = String(slides.length + zBump);

      const img = incoming.querySelector(".wipe-slider__image");
      const wipeSide = direction > 0 ? "l" : "r";

      setWipeClip(incoming, wipeSide, 100);
      gsap.set(img, { scale: 1.2 });

      const progress = { v: 100 };
      tl.to(
        progress,
        {
          v: 0,
          duration: WIPE_DURATION,
          ease,
          onUpdate() {
            const t = (100 - progress.v) / 100;
            const soft = Math.min(1, t * (1 + 0.35 * (1 - t)));
            setWipeClip(incoming, wipeSide, 100 * (1 - soft), progress.v);
          },
        },
        0
      );

      tl.to(
        img,
        {
          scale: 1,
          duration: WIPE_DURATION,
          ease,
          force3D: true,
        },
        0
      );

      tl.call(
        () => {
          if (seq !== wipeSeq) return;
          ui.bgLayers.forEach((layer, i) => {
            if (i === index) {
              setWipeClip(layer, "l", 0);
              layer.style.zIndex = String(slides.length);
            } else {
              setWipeClip(layer, "l", 100);
              layer.style.zIndex = String(i);
            }
          });
        },
        null,
        WIPE_DURATION
      );

      const lineOut = direction > 0 ? -LINE_TRAVEL : LINE_TRAVEL;
      const lineIn = direction > 0 ? LINE_TRAVEL : -LINE_TRAVEL;

      tl.call(
        () => {
          swapCharLine(
            ui.titleEl,
            slide.title,
            "wipe-slider__title-line",
            "wipe-slider__title-char",
            {
              yPercent: direction > 0 ? -120 : 120,
              y: 0,
              duration: 0.65,
              ease: "expo.inOut",
              stagger: direction > 0 ? charStagger : -charStagger,
              force3D: true,
              overwrite: true,
            },
            {
              from: { yPercent: direction > 0 ? 120 : -120, y: 0 },
              to: {
                yPercent: 0,
                y: 0,
                duration: 1.2,
                ease: "expo.inOut",
                stagger: direction > 0 ? charStagger : -charStagger,
                force3D: true,
                overwrite: true,
                delay: 0,
              },
            }
          );

          swapCharLine(
            ui.labelEl,
            slide.label,
            "wipe-slider__label-line",
            "wipe-slider__title-char",
            {
              yPercent: lineOut,
              opacity: 0,
              duration: LABEL_OUT_DUR,
              ease: TEXT_OUT,
              force3D: true,
              overwrite: true,
            },
            {
              from: {
                yPercent: lineIn,
                opacity: 0,
              },
              to: {
                yPercent: 0,
                opacity: 1,
                duration: LABEL_IN_DUR,
                ease: TEXT_SOFT,
                force3D: true,
                overwrite: true,
                delay: LABEL_IN_DELAY,
              },
            }
          );

          const pillLine = swapCharLine(
            ui.pillLabel,
            slide.title,
            "wipe-slider__pill-label-line",
            "wipe-slider__pill-char",
            {
              yPercent: lineOut,
              opacity: 0,
              duration: PILL_OUT_DUR,
              ease: TEXT_OUT,
              stagger: {
                each: PILL_CHAR_STAGGER,
                from: direction > 0 ? "start" : "end",
              },
              force3D: true,
              overwrite: true,
            },
            {
              from: {
                yPercent: lineIn,
                opacity: 0,
              },
              to: {
                yPercent: 0,
                opacity: 1,
                duration: PILL_IN_DUR,
                ease: TEXT_SOFT,
                stagger: {
                  each: PILL_CHAR_STAGGER,
                  from: direction > 0 ? "start" : "end",
                },
                force3D: true,
                overwrite: true,
                delay: PILL_IN_DELAY,
              },
            }
          );

          gsap.to(ui.pillLabel, {
            width: measurePillWidth(pillLine),
            duration: 0.75,
            ease: TEXT_SOFT,
            delay: 0.04,
            overwrite: "auto",
          });
        },
        null,
        0
      );

      const existingLines = ui.descEl.querySelectorAll(
        ".wipe-slider__desc-line"
      );
      if (existingLines.length) {
        const clone = ui.descEl.cloneNode(true);
        clone.classList.add("wipe-slider__desc-clone");
        clone.setAttribute("aria-hidden", "true");
        ui.descSlot.appendChild(clone);

        tl.to(
          clone.querySelectorAll(".wipe-slider__desc-line"),
          {
            yPercent: lineOut,
            opacity: 0,
            duration: DESC_OUT_DUR,
            ease: TEXT_OUT,
            stagger: {
              each: DESC_STAGGER,
              from: direction > 0 ? "start" : "end",
            },
            force3D: true,
            onComplete() {
              clone.remove();
            },
          },
          0
        );

        gsap.set(ui.descEl, { visibility: "hidden" });
      }

      tl.call(
        () => {
          const lines = splitDescription(slide.desc);
          gsap.set(ui.descEl, { visibility: "" });
          gsap.fromTo(
            lines,
            {
              yPercent: lineIn,
              opacity: 0,
            },
            {
              yPercent: 0,
              opacity: 1,
              duration: DESC_IN_DUR,
              ease: TEXT_IN,
              stagger: {
                each: DESC_STAGGER,
                from: direction > 0 ? "start" : "end",
              },
              force3D: true,
              overwrite: true,
              delay: DESC_IN_DELAY,
            }
          );
        },
        null,
        0
      );

      tl.call(
        () => {
          rollPercent(ui.percentEl, percentFor(index));
        },
        null,
        0.05
      );

      ui.barEls.forEach((item, i) => {
        const shouldFill = i <= index;
        const wasFilled = i <= previous;

        if (shouldFill && !wasFilled) {
          gsap.set(item.fill, {
            transformOrigin: "left center",
            opacity: 1,
          });
          tl.to(
            item.fill,
            {
              scaleX: 1,
              duration: BAR_DURATION,
              ease,
              force3D: true,
              overwrite: "auto",
            },
            0.06 * (i - previous - 1)
          );
        } else if (!shouldFill && wasFilled) {
          gsap.set(item.fill, {
            transformOrigin: "left center",
            opacity: 1,
          });
          tl.to(
            item.fill,
            {
              scaleX: 0,
              duration: 0.7 * BAR_DURATION,
              ease,
              force3D: true,
              overwrite: "auto",
            },
            0.06 * (previous - i)
          );
        }
      });
    }

    function next() {
      goTo((current + 1) % slides.length, 1);
    }

    function prev() {
      goTo((current - 1 + slides.length) % slides.length, -1);
    }

    function onKeydown(e) {
      if (e.key === "ArrowRight") next();
      else if (e.key === "ArrowLeft") prev();
    }

    function onPointerDown(e) {
      if (e.target.closest(".wipe-slider__title, .wipe-slider__info")) {
        pointerDown = false;
        return;
      }
      pointerDown = true;
      pointerStartX = e.clientX;
    }

    function onPointerUp(e) {
      if (!pointerDown) return;
      pointerDown = false;
      const delta = e.clientX - pointerStartX;
      if (Math.abs(delta) < SWIPE_THRESHOLD) return;
      if (delta < 0) next();
      else prev();
    }

    let resizeTimer = null;

    function onResize() {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(() => {
        const lines = splitDescription(slides[current].desc);
        gsap.set(lines, { yPercent: 0, y: 0, opacity: 1 });
      }, 150);
    }

    function onVisibility() {
      if (document.hidden) gsap.globalTimeline.pause();
      else gsap.globalTimeline.resume();
    }

    ui = buildStage();
    paintInitial();

    ui.prevBtn.addEventListener("click", prev);
    ui.nextBtn.addEventListener("click", next);
    ui.zonePrev.addEventListener("click", prev);
    ui.zoneNext.addEventListener("click", next);
    bindArrowMotion(ui.prevBtn, -1);
    bindArrowMotion(ui.nextBtn, 1);

    root.setAttribute("tabindex", "0");
    root.setAttribute("role", "region");
    root.setAttribute("aria-roledescription", "carousel");
    root.setAttribute("aria-label", slides[current].title);
    root.addEventListener("keydown", onKeydown);
    root.addEventListener("pointerdown", onPointerDown);
    root.addEventListener("pointerup", onPointerUp);

    gsap.matchMedia().add("(prefers-reduced-motion: reduce)", () => {
      gsap.globalTimeline.timeScale(20);
      return () => gsap.globalTimeline.timeScale(1);
    });

    window.addEventListener("resize", onResize);
    document.addEventListener("visibilitychange", onVisibility);

    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(() => {
        const lines = splitDescription(slides[current].desc);
        gsap.set(lines, { yPercent: 0, y: 0, opacity: 1 });
      });
    }

    return {
      refresh() {
        const lines = splitDescription(slides[current].desc);
        gsap.set(lines, { yPercent: 0, y: 0, opacity: 1 });
      },
      next,
      prev,
      goTo(index) {
        if (index === current || index < 0 || index >= slides.length) return;
        goTo(index, index > current ? 1 : -1);
      },
      kill() {
        window.removeEventListener("resize", onResize);
        document.removeEventListener("visibilitychange", onVisibility);
        root.removeEventListener("keydown", onKeydown);
        root.removeEventListener("pointerdown", onPointerDown);
        root.removeEventListener("pointerup", onPointerUp);
        if (descSplit) {
          descSplit.revert();
          descSplit = null;
        }
      },
    };
  }
})();
````
