JSFiddle - React, Tailwind, and code Playground

by Alexander Maslakov

HTML

<div class="carousel" x-data="carousel">
    <div class="carousel-items" x-ref="items">
        <div class="carousel-item">1</div>
        <div class="carousel-item">2</div>
        <div class="carousel-item">3</div>
    </div>
    <button
        type="button"
        class="carousel-button carousel-button-start"
        x-bind:disabled="scrolledStart"
        x-on:click="prev"
    >
        &larr;
    </button>
    <button
        type="button"
        class="carousel-button carousel-button-end"
        x-bind:disabled="scrolledEnd"
        x-on:click="next"
    >
        &rarr;
    </button>
</div>

<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>

CSS

.carousel {
    position: relative;
}

.carousel-items {
    display: flex;
    width: 100%;
    flex-direction: row;
    gap: .75rem;
    overflow-y: hidden;
    overflow-x: scroll;
    scrollbar-width: none;
    scroll-snap-type: x mandatory;
}

.carousel-item {
    display: flex;
    aspect-ratio: 16 / 9;
    min-width: 100%;
    align-items: center;
    justify-content: center;
    border-radius: .5rem;
    background-color: silver;
    scroll-snap-align: start;
}

.carousel-button {
    position: absolute;
    top: 0;
    bottom: 0;
    margin-top: auto;
    margin-bottom: auto;
    height: 2rem;
    width: 2rem;
    border-radius: 9999px;
    background-color: #000;
    padding: .25rem;
    color: rgb(255 255 255);
}

.carousel-button.carousel-button-start {
    left: .75rem;
}

.carousel-button.carousel-button-end {
    right: .75rem;
}

JavaScript

window.carousel = () => {
  let handleScroll, timer, cleanup;

  return {
    scrolledStart: false, // Флаг, який каже що контейнер .carousel-items проскролено до лівої межі
    scrolledEnd: false, // Флаг, який каже що контейнер .carousel-items проскролено до правої межі

    init() {
      // Функція, яка викликається щоразу як скорлиться контейнер .carousel-items, відслідковує
			// позицію скрола та встановлює флаги scrolledStart, scrolledEnd у відповідні значення
      handleScroll = () => {
        if (timer) {
          clearTimeout(timer);
        }

        timer = setTimeout(() => {
          this.scrolledEnd =
            Math.ceil(
              this.$refs.items.scrollLeft + this.$refs.items.clientWidth,
            ) >= Math.ceil(this.$refs.items.scrollWidth);
          this.scrolledStart = this.$refs.items.scrollLeft <= 0;
        }, 10);
      };

      handleScroll();

      this.$refs.items.addEventListener("scroll", handleScroll, false);
      addEventListener("resize", handleScroll, false);

      cleanup = () => {
        this.$refs.items.removeEventListener("scroll", handleScroll, false);
        removeEventListener("resize", handleScroll, false);
      };
    },

    destroy() {
      cleanup();
    },

    // Функція яка скролить контейнер .carousel-items на один слайд вправо (вперед)
    next() {
      const left =
        this.$refs.items.scrollLeft + this.$refs.items.clientWidth >=
        Math.ceil(this.$refs.items.scrollWidth)
          ? this.$refs.items.scrollWidth - this.$refs.items.clientWidth
          : this.$refs.items.scrollLeft + this.$refs.items.clientWidth;

      this.$refs.items.scrollTo({
        top: 0,
        left,
        behavior: "smooth",
      });
    },

    // Функція яка скролить контейнер .carousel-items на один слайд вліво (назад)
    prev() {
      const left =
        this.$refs.items.scrollLeft - this.$refs.items.clientWidth > 0
          ? this.$refs.items.scrollLeft - this.$refs.items.clientWidth
       ...