Improvements made:

  • extracted the component to a reusable function
  • made the number of slides dynamic based on viewport breakpoints
  • added a resize directive to change the number of slides if viewport is resized

source: https://alpinejs.dev/component/carousel

This component is subject to a product license.

  • Handle: @alpine-components-carousel
  • Preview:
  • Filesystem Path: components/10-other-examples/alpine-components/04-carousel/carousel.hbs
<!-- Alpine Plugins -->
<script defer src="https://unpkg.com/@alpinejs/[email protected]/dist/cdn.min.js"></script>

<!-- Alpine Core -->
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>

<style>
    [x-cloak] {
        display: none !important;
    }

    /* Chrome, Safari and Opera */
    .no-scrollbar::-webkit-scrollbar {
        display: none;
    }

    .no-scrollbar {
        -ms-overflow-style: none;
        /* IE and Edge */
        scrollbar-width: none;
        /* Firefox */
    }
</style>
<!--<h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1>-->

<!-- Smooth Scroll polyfill, for Safari -->
<script src="https://unpkg.com/[email protected]/dist/smoothscroll.js"></script>

<div class="h-screen flex justify-center items-center">

    <div x-data="carousel" @resize.window="skip()" class="flex w-full flex-col">

        <div @keydown.right="next" @keydown.left="prev" tabindex="0" role="region" aria-labelledby="carousel-label" class="flex space-x-6">
            <h2 id="carousel-label" class="sr-only" hidden>Carousel</h2>

            <!-- Prev Button -->
            <button @click="prev" class="text-6xl">
                <span aria-hidden="true">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
                    </svg>
                </span>
                <span class="sr-only">Skip to previous slide page</span>
            </button>

            <span id="carousel-content-label" class="sr-only" hidden>Carousel</span>

            <ul x-ref="slider" tabindex="0" role="listbox" aria-labelledby="carousel-content-label" class="flex w-full snap-x snap-mandatory overflow-x-scroll no-scrollbar">
                <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2" role="option">
                    <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=1" alt="placeholder image">

                    <button x-intersect:enter="$el.removeAttribute('tabindex')" x-intersect:leave="$el.setAttribute('tabindex', '-1')" class="px-4 py-2 text-sm">Do Something
                    </button>
                </li>

                <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2" role="option">
                    <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=2" alt="placeholder image">

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
                </li>

                <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2" role="option">
                    <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=3" alt="placeholder image">

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
                </li>

                <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2" role="option">
                    <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=4" alt="placeholder image">

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
                </li>

                <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2" role="option">
                    <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=5" alt="placeholder image">

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
                </li>

                <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2" role="option">
                    <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=6" alt="placeholder image">

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
                </li>
            </ul>

            <!-- Next Button -->
            <button @click="next">
                <span aria-hidden="true">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
                    </svg>
                </span>
                <span class="sr-only">Skip to next slide page</span>
            </button>

        </div>

    </div>

</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('carousel', () => ({
            // function to dynamically set the number of slides based on viewport size, using multiple ternary operators
            skip() {
                return window.innerWidth < 640 ? 1 // slides to skip on screens < 640px
                    :
                    window.innerWidth < 768 ? 2 // slides to skip on screens < 768px
                    :
                    3 // default slides to skip (larger screens)
            },
            next() {
                this.to((current, offset) => current + (offset * this.skip()))
            },
            prev() {
                this.to((current, offset) => current - (offset * this.skip()))
            },
            to(strategy) {
                let slider = this.$refs.slider;
                let current = slider.scrollLeft;
                let offset = slider.firstElementChild.getBoundingClientRect().width;
                slider.scrollTo({
                    left: strategy(current, offset),
                    behavior: 'smooth'
                })
            },
            focusableWhenVisible: {
                'x-intersect:enter'() {
                    this.$el.removeAttribute('tabindex')
                },
                'x-intersect:leave'() {
                    this.$el.setAttribute('tabindex', '-1')
                }
            }
        }))
    })
</script>
<!-- Alpine Plugins -->
<script defer src="https://unpkg.com/@alpinejs/[email protected]/dist/cdn.min.js"></script>

<!-- Alpine Core -->
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>

<style>
  [x-cloak] {
    display: none !important;
  }

  /* Chrome, Safari and Opera */
  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }

  .no-scrollbar {
    -ms-overflow-style: none; /* IE and Edge */
    scrollbar-width: none; /* Firefox */
  }
</style>
<!--<h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1>-->

<!-- Smooth Scroll polyfill, for Safari -->
<script src="https://unpkg.com/[email protected]/dist/smoothscroll.js"></script>


<div class="h-screen flex justify-center items-center">

  <div x-data="carousel"
       @resize.window="skip()"
       class="flex w-full flex-col"
  >

    <div @keydown.right="next"
         @keydown.left="prev"
         tabindex="0"
         role="region"
         aria-labelledby="carousel-label"
         class="flex space-x-6"
    >
      <h2 id="carousel-label" class="sr-only" hidden>Carousel</h2>

      <!-- Prev Button -->
      <button @click="prev" class="text-6xl">
            <span aria-hidden="true">
                <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-600" fill="none" viewBox="0 0 24 24"
                     stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round"
                                                                  d="M15 19l-7-7 7-7"/></svg>
            </span>
        <span class="sr-only">Skip to previous slide page</span>
      </button>

      <span id="carousel-content-label" class="sr-only" hidden>Carousel</span>

      <ul
              x-ref="slider"
              tabindex="0"
              role="listbox"
              aria-labelledby="carousel-content-label"
              class="flex w-full snap-x snap-mandatory overflow-x-scroll no-scrollbar"
      >
        <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2"
            role="option">
          <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=1" alt="placeholder image">

          <button x-intersect:enter="$el.removeAttribute('tabindex')"
                  x-intersect:leave="$el.setAttribute('tabindex', '-1')"
                  class="px-4 py-2 text-sm">Do Something
          </button>
        </li>

        <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2"
            role="option">
          <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=2" alt="placeholder image">

          <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
        </li>

        <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2"
            role="option">
          <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=3" alt="placeholder image">

          <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
        </li>

        <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2"
            role="option">
          <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=4" alt="placeholder image">

          <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
        </li>

        <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2"
            role="option">
          <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=5" alt="placeholder image">

          <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
        </li>

        <li class="w-full sm:w-1/3 flex-shrink-0 snap-start flex flex-col items-center justify-center p-2"
            role="option">
          <img class="mt-2 w-full" src="https://picsum.photos/400/200?random=6" alt="placeholder image">

          <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">Do Something</button>
        </li>
      </ul>

      <!-- Next Button -->
      <button @click="next">
            <span aria-hidden="true">
                <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-600" fill="none" viewBox="0 0 24 24"
                     stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round"
                                                                  d="M9 5l7 7-7 7"/></svg>
            </span>
        <span class="sr-only">Skip to next slide page</span>
      </button>

    </div>

  </div>

</div>

<script>
  document.addEventListener('alpine:init', () => {

    Alpine.data('carousel', () => ({
      // function to dynamically set the number of slides based on viewport size, using multiple ternary operators
      skip() {
        return window.innerWidth < 640 ? 1 // slides to skip on screens < 640px
              : window.innerWidth < 768 ? 2 // slides to skip on screens < 768px
              : 3 // default slides to skip (larger screens)
      },
      next() {
        this.to((current, offset) => current + (offset * this.skip()))
      },
      prev() {
        this.to((current, offset) => current - (offset * this.skip()))
      },
      to(strategy) {
        let slider = this.$refs.slider;
        let current = slider.scrollLeft;
        let offset = slider.firstElementChild.getBoundingClientRect().width;

        slider.scrollTo({left: strategy(current, offset), behavior: 'smooth'})
      },
      focusableWhenVisible: {
        'x-intersect:enter'() {
          this.$el.removeAttribute('tabindex')
        },
        'x-intersect:leave'() {
          this.$el.setAttribute('tabindex', '-1')
        }
      }

    }))
  })
</script>
/* No context defined. */