<div class="slideshow">
  <div class="slides-container">
    <div class="slides">
      <div class="slide"><img src="https://picsum.photos/600/300?v=1" alt=""></div>
      <div class="slide"><img src="https://picsum.photos/600/300?v=2" alt=""></div>
      <div class="slide"><img src="https://picsum.photos/600/300?v=3" alt=""></div>
      <div class="slide"><img src="https://picsum.photos/600/300?v=4" alt=""></div>
      <div class="slide"><img src="https://picsum.photos/600/300?v=5" alt=""></div>
    </div>
  </div>
  <div class="bullets">
  </div>
  <div class="buttons">
    <button type="button" class="prev">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
        <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
      </svg>

    </button>
    <button type="button" class="next">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
        <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
      </svg>
    </button>
  </div>
</div>
*,
*:before,
*:after {
  box-sizing: border-box;
}

:root {
  --min-margin-x: 0px;
  --max-size: 600px;
  --size: min(var(--max-size), 100vw - var(--min-margin-x));
  --duration: 1s;
  --easing: ease;
  --bullet-size: 12px;
}

body {
  perspective: 1000px;
  display: grid;
  place-items: center;
  height: 100vh;
  --3d: 0;
  &.is3d {
    --3d: 1;
  }
}

.slideshow {
  display: grid;
  grid-template-rows: 1fr auto;
  grid-template-columns: 1fr;
  gap: 10px;
  flex-direction: column;
  width: var(--size);
  transform-style: preserve-3d;
  transform: rotateY(calc(60deg * var(--3d))) rotateZ(calc(-5deg * var(--3d)));
  transition: transform 1s ease;
}

.slides-container {
  grid-column: 1;
  grid-row: 1;
  box-shadow: 0 0 0 30000px var(--shadow, #fff);
  transition: box-shadow 1s ease;
  width: var(--size);
  border-radius: clamp(
    0px,
    (100vw - var(--max-size) - var(--min-margin-x)) * 999,
    15px
  );
  transform-style: preserve-3d;
  .is3d & {
    overflow: visible;
    --shadow: rgba(0,0,0,0.5);
  }
}

.slides {
  display: flex;
  transform: translateX(calc(-1 * var(--size) * var(--slide, 0))) translateZ(calc((-150px * var(--3d)) - 0.1px));
  transition: transform var(--duration) var(--easing);
    transform-style: preserve-3d;
  
}

.slide {
  font-size: 0;
}

img {
  width: var(--size);
}

.buttons {
  transform-style: preserve-3d;
  transform: translateZ(calc(100px * var(--3d)));
  transition: transform 1s ease;
  z-index: 1;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  grid-column: 1;
  grid-row: 1;
  pointer-events: none;
  

  button {
    pointer-events: all;
    width: 40px;
    aspect-ratio: 1;
    display: flex;
    justify-content: center;
    align-items: center;
    color: #000;
    border-radius: 100vw;
    border: 1px solid #000;
    background: #fff;
    transition: all 0.2s ease;
    cursor: pointer;
    opacity: 0.4;

    &:hover {
      box-shadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.24);
      opacity: 0.8;
    }
  }
}

.bullets {
  transform-style: preserve-3d;
  transform: translateZ(calc(100px * var(--3d)));
  transition: transform 1s ease;
  grid-column: 1;
  grid-row: 2;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--bullet-size);
  place-self: center;
  height: var(--bullet-size);

  .bullet {
    aspect-ratio: 1;
    width: var(--bullet-size);
    background: #333;
    border-radius: 100vw;
    cursor: pointer;

    &.active {
      box-shadow: inset 0 0 0 2px #000;
      background: #fff;
    }
  }
}
View Compiled
window.addEventListener("load", () => {
  // We keep references to all needed elements.
  const slideshow = document.querySelector(".slideshow");
  const slidesContainer = slideshow.querySelector(".slides");
  const slides = slideshow.querySelectorAll(".slide");
  const bulletContainer = slideshow.querySelector(".bullets");
  const prev = slideshow.querySelector(".prev");
  const next = slideshow.querySelector(".next");

  const length = slides.length;

  // We use Proxy to include some kind of reactivity.
  // By just changing active.slide, the DOM gets
  // changed automatically, without the need to
  // make the DOM changes within the event handlers.
  const active = new Proxy(
    { slide: 0 },
    {
      set(obj, prop, value) {
        if (prop == "slide") {
          // If we click "prev" while on the first slide
          // then go to the last one.
          if (value < 0) {
            value = length - 1;
          }

          // If we click "next" while on the last slide
          // then go to the first one.
          if (value >= length) {
            value = 0;
          }

          // Make the appropriate DOM changes
          slidesContainer.style.setProperty("--slide", value);
          bullets[Reflect.get(obj, prop)].classList.remove("active");
          bullets[value].classList.add("active");
        }

        // Actually set the active.slide prop to it's new value
        return Reflect.set(obj, prop, value)
      }
    }
  );

  // Automatically create the bullets based on
  // the amount of slides we have.
  const fragment = document.createDocumentFragment();
  slides.forEach((slide, index) => {
    const bullet = document.createElement("span");
    bullet.classList.add("bullet");
    if (!index) {
      // Set the first bullet as active
      bullet.classList.add("active");
    }
    // Add the click functionality to the bullet
    bullet.addEventListener("click", () => active.slide = index);
    fragment.appendChild(bullet);
  });
  bulletContainer.appendChild(fragment);

  // Keep a reference to the bullets
  // so we can add/remove the active class
  // within the proxy handler for active.slide.
  const bullets = bulletContainer.querySelectorAll(".bullet");

  // Add the prev/next functionality to the buttons
  prev.addEventListener("click", () => active.slide--);
  next.addEventListener("click", () => active.slide++);
  
  document.addEventListener('keydown', e => {
    if (e.key == ' ') {
      e.preventDefault();
      document.body.classList.toggle('is3d')
    }
  });
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.