<div class="slider">
  <button class="slider--btn slider--btn__prev">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="m15 18-6-6 6-6" />
    </svg>
  </button>

  <div class="slides__wrapper">
    <div class="slides">
      <!-- slide 1 -->
      <div class="slide" data-current>
        <div class="slide__inner">
          <div class="slide--image__wrapper">
            <img class="slide--image" src="https://devloop01.github.io/voyage-slider/images/scotland-mountains.jpg" alt="Image 1" />
          </div>
        </div>
      </div>
      <div class="slide__bg" style="--bg: url(https://devloop01.github.io/voyage-slider/images/scotland-mountains.jpg); --dir: 0" data-current></div>

      <!-- slide 2 -->
      <div class="slide" data-next>
        <div class="slide__inner">
          <div class="slide--image__wrapper">
            <img class="slide--image" src="
https://devloop01.github.io/voyage-slider/images/machu-pichu.jpg" alt="Image 2" />
          </div>
        </div>
      </div>
      <div class="slide__bg" style="--bg: url(https://devloop01.github.io/voyage-slider/images/machu-pichu.jpg); --dir: 1" data-next></div>

      <!-- slide 3 -->
      <div class="slide" data-previous>
        <div class="slide__inner">
          <div class="slide--image__wrapper">
            <img class="slide--image" src="https://devloop01.github.io/voyage-slider/images/chamonix.jpg" alt="Image 3" />
          </div>
        </div>
      </div>
      <div class="slide__bg" style="--bg: url(https://devloop01.github.io/voyage-slider/images/chamonix.jpg); --dir: -1" data-previous></div>
    </div>
    <div class="slides--infos">
      <!-- Slide Info 1 -->
      <div class="slide-info" data-current>
        <div class="slide-info__inner">
          <div class="slide-info--text__wrapper">
            <div data-title class="slide-info--text">
              <span>Highlands</span>
            </div>
            <div data-subtitle class="slide-info--text">
              <span>Scotland</span>
            </div>
            <div data-description class="slide-info--text">
              <span>The mountains are calling</span>
            </div>
          </div>
        </div>
      </div>

      <!-- Slide Info 2 -->
      <div class="slide-info" data-next>
        <div class="slide-info__inner">
          <div class="slide-info--text__wrapper">
            <div data-title class="slide-info--text">
              <span>Machu Pichu</span>
            </div>
            <div data-subtitle class="slide-info--text">
              <span>Peru</span>
            </div>
            <div data-description class="slide-info--text">
              <span>Adventure is never far away</span>
            </div>
          </div>
        </div>
      </div>

      <!-- Slide Info 3 -->
      <div class="slide-info" data-previous>
        <div class="slide-info__inner">
          <div class="slide-info--text__wrapper">
            <div data-title class="slide-info--text">
              <span>Chamonix</span>
            </div>
            <div data-subtitle class="slide-info--text">
              <span>France</span>
            </div>
            <div data-description class="slide-info--text">
              <span>Let your dreams come true</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <button class="slider--btn slider--btn__next">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="m9 18 6-6-6-6" />
    </svg>
  </button>
</div>

<div class="loader">
  <span class="loader__text">0%</span>
</div>

<div class="support">
  <a href="https://twitter.com/DevLoop01" target="_blank">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z" />
    </svg>
  </a>
  <a href="https://github.com/devloop01/voyage-slider" target="_blank">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
      <path d="M9 18c-4.51 2-5-2-7-2" />
    </svg>
  </a>
</div>
@import url("https://api.fontshare.com/v2/css?f[]=archivo@100,200,300,400,500,600,700,800,900&f[]=clash-display@200,300,400,500,600,700&display=swap");

:root {
  --slide-width: min(25vw, 300px);
  --slide-aspect: 2 / 3;

  --slide-transition-duration: 800ms;
  --slide-transition-easing: ease;

  --font-archivo: "Archivo", sans-serif;
  --font-clash-display: "Clash Display", sans-serif;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  width: 100%;
  height: 100%;
}

body {
  display: grid;
  place-items: center;
  overflow: hidden;

  background: rgba(0, 0, 0, 0.787);
}

button {
  border: none;
  background: none;
  cursor: pointer;
  &:focus {
    outline: none;
    border: none;
  }
}

/* ------------------------------------------------ */
/* -------------------- SLIDER -------------------- */
/* ------------------------------------------------ */

.slider {
  width: calc(3 * var(--slide-width));
  height: calc(2 * var(--slide-height));
  display: flex;
  align-items: center;
}

.slider--btn {
  --size: 40px;

  display: inline-flex;
  justify-content: center;
  align-items: center;
  opacity: 0.7;
  transition: opacity 250ms cubic-bezier(0.215, 0.61, 0.355, 1);
  z-index: 999;

  & svg {
    width: var(--size);
    height: var(--size);
    stroke: white;
  }

  &:hover {
    opacity: 1;
  }
}

.slides__wrapper {
  width: 100%;
  height: 100%;

  display: grid;
  place-items: center;

  & > * {
    grid-area: 1 / -1;
  }
}

.slides,
.slides--infos {
  width: 100%;
  height: 100%;

  pointer-events: none;

  display: grid;
  place-items: center;
  & > * {
    grid-area: 1 / -1;
  }
}

/* ------------------------------------------------ */
/* -------------------- SLIDE --------------------- */
/* ------------------------------------------------ */

.slide {
  --slide-tx: 0px;
  --slide-ty: 0vh;
  --padding: 0px;
  --offset: 0;

  width: var(--slide-width);
  height: auto;
  aspect-ratio: var(--slide-aspect);
  user-select: none;
  perspective: 800px;

  transform: perspective(1000px)
    translate3d(var(--slide-tx), var(--slide-ty), var(--slide-tz, 0))
    rotateY(var(--slide-rotY)) scale(var(--slide-scale));
  transition: transform var(--slide-transition-duration)
    var(--slide-transition-easing);
}

.slide[data-current] {
  --slide-scale: 1.2;
  --slide-tz: 0px;
  --slide-tx: 0px;
  --slide-rotY: 0;

  pointer-events: auto;
}

.slide[data-next] {
  --slide-tx: calc(1 * var(--slide-width) * 1.07);
  --slide-rotY: -45deg;
}

.slide[data-previous] {
  --slide-tx: calc(-1 * var(--slide-width) * 1.07);
  --slide-rotY: 45deg;
}

.slide:not([data-current]) {
  --slide-scale: 1;
  --slide-tz: 0;
  /* --slide-tx: calc(var(--offset) * var(--slide-width) * 1.05); */
  /* --slide-rotY: calc(var(--dir) * -45deg); */

  pointer-events: none;
}

.slide[data-current] {
  & .slide--image {
    filter: brightness(0.8);
  }
}

.slide:not([data-current]) {
  & .slide--image {
    filter: brightness(0.5);
  }
}

.slide__inner {
  --rotX: 0;
  --rotY: 0;
  --bgPosX: 0%;
  --bgPosY: 0%;

  position: relative;
  left: calc(var(--padding) / 2);
  top: calc(var(--padding) / 2);
  width: calc(100% - var(--padding));
  height: calc(100% - var(--padding));
  transform-style: preserve-3d;
  transform: rotateX(var(--rotX)) rotateY(var(--rotY));
}

.slide--image__wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.slide--image {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 50%;
  left: 50%;
  object-fit: cover;
  transform: translate(-50%, -50%) scale(1.25)
    translate3d(var(--bgPosX), var(--bgPosY), 0);
  transition: filter var(--slide-transition-duration)
    var(--slide-transition-easing);
}

.slide__bg {
  position: fixed;
  inset: -20%;
  background-image: var(--bg);
  background-size: cover;
  background-position: center center;

  z-index: -1;
  pointer-events: none;

  transition: opacity var(--slide-transition-duration) ease,
    transform var(--slide-transition-duration) ease;

  &::before {
    content: "";
    position: absolute;
    inset: 0;
  }

  &::before {
    background: rgba(0, 0, 0, 0.8);
    backdrop-filter: blur(8px);
  }

  &:not([data-current]) {
    opacity: 0;
  }

  &[data-previous] {
    transform: translateX(-10%);
  }

  &[data-next] {
    transform: translateX(10%);
  }
}

/* ------------ SLIDE INFO ---------------- */

.slide-info {
  --padding: 0px;

  position: relative;
  width: var(--slide-width);
  height: 100%;
  aspect-ratio: var(--slide-aspect);
  user-select: none;
  perspective: 800px;
  z-index: 100;
}

.slide-info[data-current] {
  & .slide-info--text span {
    opacity: 1;
    transform: translate3d(0, 0, 0);
    transition-delay: 250ms;
  }
}

.slide-info:not([data-current]) {
  & .slide-info--text span {
    opacity: 0;
    transform: translate3d(0, 100%, 0);
    transition-delay: 0ms;
  }
}

.slide-info__inner {
  position: relative;
  left: calc(var(--padding) / 2);
  top: calc(var(--padding) / 2);
  width: calc(100% - var(--padding));
  height: calc(100% - var(--padding));
  transform-style: preserve-3d;
  transform: rotateX(var(--rotX)) rotateY(var(--rotY));
}

.slide-info--text__wrapper {
  --z-offset: 45px;

  position: absolute;
  height: fit-content;
  left: -15%;
  bottom: 15%;
  transform: translateZ(var(--z-offset));
  z-index: 2;
  pointer-events: none;
}

.slide-info--text {
  font-family: var(--font-clash-display);
  color: #fff;
  overflow: hidden;

  & span {
    display: block;
    white-space: nowrap;
    transition: var(--slide-transition-duration) var(--slide-transition-easing);
    transition-property: opacity, transform;
  }

  &[data-title],
  &[data-subtitle] {
    font-size: min(3cqw, 2.4rem);
    font-weight: 800;
    letter-spacing: 0.2cqw;
    white-space: nowrap;
    text-transform: uppercase;
  }

  &[data-subtitle] {
    margin-left: 2cqw;
    font-size: min(2.2cqw, 1.8rem);
    font-weight: 600;
  }

  &[data-description] {
    margin-left: 1cqw;
    font-size: min(1.5cqw, 0.95rem);
    font-family: var(--font-archivo);
    font-weight: 300;
  }
}

/* ------------------------------------------------ */
/* -------------------- LOADER --------------------- */
/* ------------------------------------------------ */

.loader {
  position: fixed;
  inset: 0;

  display: grid;
  place-items: center;

  background: #000;
  z-index: 1000;

  opacity: 1;
  transition: opacity 0.5s ease-out;

  .loader__text {
    font-family: var(--font-clash-display);
    font-size: clamp(2rem, 2vw, 5rem);
    font-weight: 800;
    color: #fff;
  }
}

/* ------------------------------------------- */

.support {
  position: absolute;
  right: 10px;
  bottom: 10px;
  padding: 10px;
  display: flex;
  a {
    margin: 0 10px;
    color: #fff;
    font-size: 1.8rem;
    backface-visibility: hidden;
    transition: all 150ms ease;
    &:hover {
      transform: scale(1.1);
    }
  }
}
import imagesLoaded from "https://esm.sh/imagesloaded";

console.clear();

// -------------------------------------------------
// ------------------ Utilities --------------------
// -------------------------------------------------

// Math utilities
const wrap = (n, max) => (n + max) % max;
const lerp = (a, b, t) => a + (b - a) * t;

// DOM utilities
const isHTMLElement = (el) => el instanceof HTMLElement;

const genId = (() => {
  let count = 0;
  return () => {
    return (count++).toString();
  };
})();

class Raf {
  constructor() {
    this.rafId = 0;
    this.raf = this.raf.bind(this);
    this.callbacks = [];

    this.start();
  }

  start() {
    this.raf();
  }

  stop() {
    cancelAnimationFrame(this.rafId);
  }

  raf() {
    this.callbacks.forEach(({ callback, id }) => callback({ id }));
    this.rafId = requestAnimationFrame(this.raf);
  }

  add(callback, id) {
    this.callbacks.push({ callback, id: id || genId() });
  }

  remove(id) {
    this.callbacks = this.callbacks.filter((callback) => callback.id !== id);
  }
}

class Vec2 {
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }

  set(x, y) {
    this.x = x;
    this.y = y;
  }

  lerp(v, t) {
    this.x = lerp(this.x, v.x, t);
    this.y = lerp(this.y, v.y, t);
  }
}

const vec2 = (x = 0, y = 0) => new Vec2(x, y);

export function tilt(node, options) {
  let { trigger, target } = resolveOptions(node, options);

  let lerpAmount = 0.06;

  const rotDeg = { current: vec2(), target: vec2() };
  const bgPos = { current: vec2(), target: vec2() };

  const update = (newOptions) => {
    destroy();
    ({ trigger, target } = resolveOptions(node, newOptions));
    init();
  };

  let rafId;

  function ticker({ id }) {
    rafId = id;

    rotDeg.current.lerp(rotDeg.target, lerpAmount);
    bgPos.current.lerp(bgPos.target, lerpAmount);

    for (const el of target) {
      el.style.setProperty("--rotX", rotDeg.current.y.toFixed(2) + "deg");
      el.style.setProperty("--rotY", rotDeg.current.x.toFixed(2) + "deg");

      el.style.setProperty("--bgPosX", bgPos.current.x.toFixed(2) + "%");
      el.style.setProperty("--bgPosY", bgPos.current.y.toFixed(2) + "%");
    }
  }

  const onMouseMove = ({ offsetX, offsetY }) => {
    lerpAmount = 0.1;

    for (const el of target) {
      const ox = (offsetX - el.clientWidth * 0.5) / (Math.PI * 3);
      const oy = -(offsetY - el.clientHeight * 0.5) / (Math.PI * 4);

      rotDeg.target.set(ox, oy);
      bgPos.target.set(-ox * 0.3, oy * 0.3);
    }
  };

  const onMouseLeave = () => {
    lerpAmount = 0.06;

    rotDeg.target.set(0, 0);
    bgPos.target.set(0, 0);
  };

  const addListeners = () => {
    trigger.addEventListener("mousemove", onMouseMove);
    trigger.addEventListener("mouseleave", onMouseLeave);
  };

  const removeListeners = () => {
    trigger.removeEventListener("mousemove", onMouseMove);
    trigger.removeEventListener("mouseleave", onMouseLeave);
  };

  const init = () => {
    addListeners();
    raf.add(ticker);
  };

  const destroy = () => {
    removeListeners();
    raf.remove(rafId);
  };

  init();

  return { destroy, update };
}

function resolveOptions(node, options) {
  return {
    trigger: options?.trigger ?? node,
    target: options?.target
      ? Array.isArray(options.target)
        ? options.target
        : [options.target]
      : [node]
  };
}

// -----------------------------------------------------

// Global Raf Instance
const raf = new Raf();

function init() {
  const loader = document.querySelector(".loader");

  const slides = [...document.querySelectorAll(".slide")];
  const slidesInfo = [...document.querySelectorAll(".slide-info")];

  const buttons = {
    prev: document.querySelector(".slider--btn__prev"),
    next: document.querySelector(".slider--btn__next")
  };

  loader.style.opacity = 0;
  loader.style.pointerEvents = "none";

  slides.forEach((slide, i) => {
    const slideInner = slide.querySelector(".slide__inner");
    const slideInfoInner = slidesInfo[i].querySelector(".slide-info__inner");

    tilt(slide, { target: [slideInner, slideInfoInner] });
  });

  buttons.prev.addEventListener("click", change(-1));
  buttons.next.addEventListener("click", change(1));
}

function setup() {
  const loaderText = document.querySelector(".loader__text");

  const images = [...document.querySelectorAll("img")];
  const totalImages = images.length;
  let loadedImages = 0;
  let progress = {
    current: 0,
    target: 0
  };

  // update progress target
  images.forEach((image) => {
    imagesLoaded(image, (instance) => {
      if (instance.isComplete) {
        loadedImages++;
        progress.target = loadedImages / totalImages;
      }
    });
  });

  // lerp progress current to progress target
  raf.add(({ id }) => {
    progress.current = lerp(progress.current, progress.target, 0.06);

    const progressPercent = Math.round(progress.current * 100);
    loaderText.textContent = `${progressPercent}%`;

    // hide loader when progress is 100%
    if (progressPercent === 100) {
      init();

      // remove raf callback when progress is 100%
      raf.remove(id);
    }
  });
}

function change(direction) {
  return () => {
    let current = {
      slide: document.querySelector(".slide[data-current]"),
      slideInfo: document.querySelector(".slide-info[data-current]"),
      slideBg: document.querySelector(".slide__bg[data-current]")
    };
    let previous = {
      slide: document.querySelector(".slide[data-previous]"),
      slideInfo: document.querySelector(".slide-info[data-previous]"),
      slideBg: document.querySelector(".slide__bg[data-previous]")
    };
    let next = {
      slide: document.querySelector(".slide[data-next]"),
      slideInfo: document.querySelector(".slide-info[data-next]"),
      slideBg: document.querySelector(".slide__bg[data-next]")
    };

    Object.values(current).map((el) => el.removeAttribute("data-current"));
    Object.values(previous).map((el) => el.removeAttribute("data-previous"));
    Object.values(next).map((el) => el.removeAttribute("data-next"));

    if (direction === 1) {
      let temp = current;
      current = next;
      next = previous;
      previous = temp;

      current.slide.style.zIndex = "20";
      previous.slide.style.zIndex = "30";
      next.slide.style.zIndex = "10";
    } else if (direction === -1) {
      let temp = current;
      current = previous;
      previous = next;
      next = temp;

      current.slide.style.zIndex = "20";
      previous.slide.style.zIndex = "10";
      next.slide.style.zIndex = "30";
    }

    Object.values(current).map((el) => el.setAttribute("data-current", ""));
    Object.values(previous).map((el) => el.setAttribute("data-previous", ""));
    Object.values(next).map((el) => el.setAttribute("data-next", ""));
  };
}

// Start
setup();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.