# Expo Slider

> A Swiper expo-effect carousel with scale, offset, and grayscale depth — cinematic horizontal image galleries.

**Definition:** Expo Slider is a sliders UI animation from collections.html. It is built with HTML, CSS, and JavaScript, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Sliders
- Stack: HTML, CSS, and JavaScript
- Added: 2026-08-06
- Canonical page: https://html.bosconvertpro.com/collection/expo-slider
- Full-page preview: https://html.bosconvertpro.com/collection/expo-slider/full
- Live demo: https://html.bosconvertpro.com/demos/expo-slider/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/expo-slider/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Expo Slider?

Expo Slider is a sliders UI animation from collections.html. A Swiper expo-effect carousel with scale, offset, and grayscale depth — cinematic horizontal image galleries.

### What is Expo Slider built with?

Expo Slider is built with HTML, CSS, and JavaScript. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use Expo Slider in my project?

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

### Is Expo Slider free to use?

Yes. The Expo 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>Expo Slider</title>
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <!--
      Source: UI Initiative Expo Slider (code.txt)
      Horizontal only · Expo effect · Unsplash images
    -->
    <div class="swiper">
      <div class="swiper-wrapper">
        <div class="swiper-slide">
          <div class="expo-container">
            <img
              class="expo-image"
              src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1800&q=85"
              alt="Snow-capped mountain range under soft clouds"
              draggable="false"
            />
          </div>
        </div>

        <div class="swiper-slide">
          <div class="expo-container">
            <img
              class="expo-image"
              src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=1800&q=85"
              alt="Sunlight over mountain landscape"
              draggable="false"
            />
          </div>
        </div>

        <div class="swiper-slide">
          <div class="expo-container">
            <img
              class="expo-image"
              src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=1800&q=85"
              alt="Sunlit forest path through tall trees"
              draggable="false"
            />
          </div>
        </div>

        <div class="swiper-slide">
          <div class="expo-container">
            <img
              class="expo-image"
              src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1800&q=85"
              alt="Foggy hills at dawn"
              draggable="false"
            />
          </div>
        </div>

        <div class="swiper-slide">
          <div class="expo-container">
            <img
              class="expo-image"
              src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=1800&q=85"
              alt="Lake and mountains at golden hour"
              draggable="false"
            />
          </div>
        </div>

        <div class="swiper-slide">
          <div class="expo-container">
            <img
              class="expo-image"
              src="https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?auto=format&fit=crop&w=1800&q=85"
              alt="Calm lake reflecting mountain peaks"
              draggable="false"
            />
          </div>
        </div>

        <div class="swiper-slide">
          <div class="expo-container">
            <img
              class="expo-image"
              src="https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1800&q=85"
              alt="Starry night sky over mountain peaks"
              draggable="false"
            />
          </div>
        </div>
      </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
````

## Source — styles.css
````css
/* ============================================================
   Expo Slider styles
   From original bundle index-De2oKvi-.css (horizontal only)
   ============================================================ */

html,
body {
  position: relative;
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  background: #0f0f11;
  color: #fff;
  line-height: 1.5;
  font-family:
    -apple-system,
    system-ui,
    "Helvetica Neue",
    Helvetica,
    Arial,
    "Segoe UI",
    Roboto,
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

:root {
  --expo-image-border-radius: 16px;
  --expo-image-offset: 1.25;
  --expo-padding: 0px;
}

@media (min-width: 768px) {
  :root {
    --expo-image-border-radius: 32px;
  }
}

/* Stage */
.swiper {
  max-width: 100%;
  width: 100%;
  aspect-ratio: 16 / 9;
  margin: auto 0;
}

/* Expo effect chrome (original .swiper-expo) */
.swiper-expo {
  box-sizing: border-box;
}

.swiper-expo.swiper-horizontal {
  padding-top: var(--expo-padding, 0px);
  padding-bottom: var(--expo-padding, 0px);
}

.swiper-expo .expo-container {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  overflow: hidden;
  border-radius: var(--expo-image-border-radius, 0px);
  background: #1a1a1a;
}

.swiper-expo .expo-image {
  width: 100%;
  height: 100%;
  overflow: hidden;
  position: absolute;
  object-fit: cover;
  display: block;
  pointer-events: none;
  user-select: none;
}

/* Oversized image for parallax travel (imageOffset) */
.swiper-expo.swiper-horizontal .expo-image {
  left: calc((var(--expo-image-offset) * 100% - 100%) / -2);
  top: 0;
  width: calc(var(--expo-image-offset) * 100%);
  height: 100%;
}

.swiper-slide {
  height: 100%;
  overflow: visible;
}
````

## Source — script.js
````javascript
/**
 * Expo Slider
 * Clean port of UI Initiative Expo effect from the shared demo
 * (assets/index-C_hr3U6n.js → modules qt + jt).
 *
 * Original init:
 *   new Swiper(".swiper", {
 *     direction: "horizontal",
 *     modules: [ExpoEffect],
 *     effect: "expo",
 *     slidesPerView: 1.5,
 *     expoEffect: { imageScale: 1.125, imageOffset: 1.25, scale: 1.25, rotate: 0, grayscale: true },
 *     grabCursor: true,
 *     spaceBetween: 16,
 *     breakpoints: { 768: { spaceBetween: 32 } },
 *   })
 */
(function () {
  "use strict";

  if (typeof Swiper === "undefined") {
    console.error("[Expo Slider] Swiper failed to load");
    return;
  }

  /**
   * effectInit — same helper Swiper premium effects use (jt in the bundle).
   * Registers class names, hooks setTranslate / setTransition, forces params.
   */
  function effectInit(params) {
    var effect = params.effect;
    var swiper = params.swiper;
    var on = params.on;
    var setTranslate = params.setTranslate;
    var setTransition = params.setTransition;
    var overwriteParams = params.overwriteParams;
    var perspective = params.perspective;
    var recreateShadows = params.recreateShadows;
    var getEffectParams = params.getEffectParams;

    on("beforeInit", function () {
      if (swiper.params.effect !== effect) return;

      swiper.classNames.push(swiper.params.containerModifierClass + effect);

      if (perspective && perspective()) {
        swiper.classNames.push(swiper.params.containerModifierClass + "3d");
      }

      var over = overwriteParams ? overwriteParams() : {};
      Object.assign(swiper.params, over);
      Object.assign(swiper.originalParams, over);
    });

    on("setTranslate _virtualUpdated", function () {
      if (swiper.params.effect === effect) setTranslate();
    });

    on("setTransition", function (_s, duration) {
      if (swiper.params.effect === effect) setTransition(duration);
    });

    on("transitionEnd", function () {
      if (swiper.params.effect !== effect || !recreateShadows) return;
      if (!getEffectParams || !getEffectParams().slideShadows) return;
      swiper.slides.forEach(function (slide) {
        slide
          .querySelectorAll(
            ".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left"
          )
          .forEach(function (el) {
            el.remove();
          });
      });
      recreateShadows();
    });

    var virtualNeedsUpdate;
    on("virtualUpdate", function () {
      if (swiper.params.effect !== effect) return;
      if (!swiper.slides.length) virtualNeedsUpdate = true;
      requestAnimationFrame(function () {
        if (
          virtualNeedsUpdate &&
          swiper.slides &&
          swiper.slides.length
        ) {
          setTranslate();
          virtualNeedsUpdate = false;
        }
      });
    });
  }

  /**
   * ExpoEffect module — exact port of `qt` from the original bundle.
   *
   * Per-slide (progress p ∈ [-1, 1]):
   *   image:     translateX(p·u·100%) scale(1+(imageScale-1)·|p|)
   *              filter: grayscale(|p|)
   *              origin: p>0 ? left : right
   *   container: scale(1+(scale-1)·|p|) rotateY(rotate·p)
   *              origin: p>0 ? right : left
   *   content:   translateX(p·100%) opacity(1-2·|p|)   [optional]
   *
   * --expo-padding lets side slides (scaled up) fill the cross-axis.
   */
  function ExpoEffect(ctx) {
    var swiper = ctx.swiper;
    var on = ctx.on;
    var extendParams = ctx.extendParams;

    extendParams({
      expoEffect: {
        imageScale: 1.125,
        imageOffset: 1.25,
        scale: 1.25,
        rotate: 0,
        grayscale: true,
      },
    });

    /** setTranslate — original `i` */
    function setTranslate() {
      var slides = swiper.slides;
      var rtlTranslate = swiper.rtlTranslate;
      var slidesPerView = swiper.params.slidesPerView;
      var isHorizontal = swiper.isHorizontal();
      var params = swiper.params.expoEffect;

      // Parallax amount for the oversized image
      var parallaxFactor = 0.5;
      var imageOffset = Math.max(1.25, params.imageOffset);

      if (slidesPerView > 1.5) {
        var edge = (imageOffset - 1) / 2 / imageOffset;
        parallaxFactor = Math.max(edge, 0.5 - (slidesPerView - 1.5));
      }

      var imageScale = Math.max(1.125, params.imageScale);
      var containerScale = Math.max(1.25, params.scale);
      var rtl = rtlTranslate ? -1 : 1;

      for (var i = 0; i < slides.length; i += 1) {
        var slide = slides[i];
        var container = slide.querySelector(".expo-container");
        var content = slide.querySelector(".expo-content");
        var image = slide.querySelector(".expo-image");
        var progress = slide.progress;
        var p = Math.max(Math.min(progress, 1), -1);
        var abs = Math.abs(p);

        // Image: parallax + scale + grayscale
        if (image) {
          var axis = isHorizontal ? "X" : "Y";
          image.style.transform =
            "translate" +
            axis +
            "(" +
            p * parallaxFactor * 100 * rtl +
            "%) scale(" +
            (1 + (imageScale - 1) * abs) +
            ")";

          if (params.grayscale) {
            image.style.filter = "grayscale(" + abs + ")";
          }
        }

        // Transform origins (outer edge of travel)
        var origins = isHorizontal
          ? rtlTranslate
            ? ["right", "left"]
            : ["left", "right"]
          : ["top", "bottom"];

        if (Math.abs(progress) > 0.01) {
          if (image) {
            image.style.transformOrigin =
              progress > 0 ? origins[0] : origins[1];
          }
          if (container) {
            container.style.transformOrigin =
              progress > 0 ? origins[1] : origins[0];
          }
        }

        // Container: scale UP on sides + optional 3D rotate
        if (container) {
          var rotAxis = isHorizontal ? "Y" : "X";
          var rotSign = isHorizontal ? 1 : -1;
          container.style.transform =
            "scale(" +
            (1 + (containerScale - 1) * abs) +
            ") rotate" +
            rotAxis +
            "(" +
            params.rotate * p * rotSign * rtl +
            "deg)";
        }

        // Optional caption
        if (content) {
          var cAxis = isHorizontal ? "X" : "Y";
          content.style.transform =
            "translate" + cAxis + "(" + p * 100 * rtl + "%)";
          content.style.opacity = String(1 - abs * 2);
        }
      }
    }

    /** setTransition — original `r` */
    function setTransition(duration) {
      var slides = swiper.slides;
      for (var i = 0; i < slides.length; i += 1) {
        var els = slides[i].querySelectorAll(
          ".expo-container, .expo-image, .expo-content"
        );
        for (var j = 0; j < els.length; j += 1) {
          els[j].style.transitionDuration = duration + "ms";
        }
      }
    }

    /**
     * setPadding — original `n`
     * Side cards scale up to `scale`; padding reserves room on the cross-axis.
     */
    function setPadding() {
      var rect = swiper.el.getBoundingClientRect();
      var cross = swiper.isHorizontal() ? rect.height : rect.width;
      var rotate = swiper.params.expoEffect.rotate;
      var scale = swiper.params.expoEffect.scale;
      var imageOffset = swiper.params.expoEffect.imageOffset;

      swiper.el.style.setProperty("--expo-image-offset", String(imageOffset));

      var current = swiper.el.style.getPropertyValue("--expo-padding");
      var currentPx = parseInt(current, 10) || 0;

      var inner = cross / scale;
      var pad = (cross - inner) / 2;
      if (rotate) pad = pad * 1.35;
      pad = Math.round(pad);

      if (
        current &&
        !Number.isNaN(currentPx) &&
        Math.abs(pad - currentPx) < 5
      ) {
        return;
      }
      swiper.el.style.setProperty("--expo-padding", pad + "px");
    }

    on("init", setPadding);
    on("resize", setPadding);
    on("update", setPadding);

    effectInit({
      effect: "expo",
      swiper: swiper,
      on: on,
      setTranslate: setTranslate,
      setTransition: setTransition,
      perspective: function () {
        return true;
      },
      overwriteParams: function () {
        return {
          centeredSlides: true,
          slidesPerGroup: 1,
          watchSlidesProgress: true,
        };
      },
    });
  }

  // Register on full CDN bundle
  if (typeof Swiper.use === "function") {
    Swiper.use([ExpoEffect]);
  }

  // Same config as the original demo (horizontal only)
  new Swiper(".swiper", {
    direction: "horizontal",
    modules: [ExpoEffect],
    effect: "expo",
    slidesPerView: 1.5,
    expoEffect: {
      imageScale: 1.125,
      imageOffset: 1.25,
      scale: 1.25,
      rotate: 0,
      grayscale: true,
    },
    grabCursor: true,
    spaceBetween: 16,
    breakpoints: {
      768: {
        spaceBetween: 32,
      },
    },
  });
})();
````
