<div class="slideshow">
  <div class="slides-container">
    <div class="slides">
      <img class="slide" src="https://picsum.photos/600/300?v=1" alt="">
      <img class="slide" src="https://picsum.photos/600/300?v=2" alt="">
      <img class="slide" src="https://picsum.photos/600/300?v=3" alt="">
      <img class="slide" src="https://picsum.photos/600/300?v=4" alt="">
      <img class="slide" src="https://picsum.photos/600/300?v=5" alt="">
    </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: 0.3s;
  --easing: ease;
  --bullet-size: 12px;
}

body {
  display: grid;
  place-items: center;
  height: 100vh;
}

.slideshow {
  display: grid;
  grid-template-rows: 1fr auto;
  grid-template-columns: 1fr;
  gap: 10px;
  width: var(--size);
}

.slides-container {
  grid-column: 1;
  grid-row: 1;
  overflow: hidden;
  border-radius: clamp(
    0px,
    (100vw - var(--max-size) - var(--min-margin-x)) * 999,
    15px
  );
}

.slides {
  display: flex;
  transform: translateX(calc(-1 * var(--size) * var(--slide, 0)));
  transition: transform var(--duration) var(--easing);
}

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

.buttons {
  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: none;
    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 {
  grid-column: 1;
  grid-row: 2;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--bullet-size);
  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[obj[prop]].classList.remove("active");
          bullets[value].classList.add("active");
        }

        // Actually set the active.slide prop to it's new value
        obj[prop] = value;
        return true;
      }
    }
  );

  // 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++);
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.