Creating a Carousel with Tailwind CSS and Alpine.js

Carousels are a popular way to showcase multiple items or images in a confined space on a website. In this blog post, we'll walk you through how to create a responsive carousel using Tailwind CSS and Alpine.js. We'll break down each section of the code and explain its functionality.

Prerequisites:

  1. Basic understanding of HTML and CSS.
  2. Familiarity with Tailwind CSS and Alpine.js.

Step 1: Importing the Required Libraries

Before diving into the carousel, we need to import a smooth scrolling polyfill to ensure smooth transitions between slides:

<script src="https://unpkg.com/smoothscroll-polyfill@0.4.4/dist/smoothscroll.js"></script>

Step 2: Setting up the Carousel Structure

The main structure of the carousel is wrapped inside a div with a class of relative. This ensures that all the child elements positioned absolutely (like the navigation buttons) are relative to this container.

<div class="relative">
    <!-- Carousel content goes here -->
</div>

Step 3: Initializing Alpine.js

Within the main container, we initialize Alpine.js using the x-data attribute. This attribute contains the data and methods required for the carousel's functionality:

  • currentSlide: Keeps track of the current slide.
  • skip: Determines how many slides to skip when navigating.
  • atBeginning & atEnd: Flags to check if the carousel is at the beginning or end.
  • autoSlideInterval: Handles the auto-slide functionality. Navigation methods: startAutoSlide, stopAutoSlide, goToSlide, next, prev, and more.
<div x-data="{...}" x-init="startAutoSlide()" @mouseover="stopAutoSlide()" @mouseout="startAutoSlide()" class="flex flex-col w-full">
    <!-- Carousel slides and controls go here -->
</div>

Step 4: Adding Carousel Slides

The slides are added within an unordered list (ul) with a reference x-ref="slider". Each slide is represented by a list item (li) containing an image and a button to display the current slide index:

<ul x-ref="slider" @scroll="updateCurrentSlide" tabindex="0" role="listbox" aria-labelledby="carousel-content-label" class="flex w-full overflow-x-hidden snap-x snap-mandatory">
    <!-- Slide 1 -->
    <li x-bind="disableNextAndPreviousButtons" class="flex flex-col items-center justify-center w-full p-0 shrink-0 snap-start" role="option">
        <img class="w-full " src="https://picsum.photos/400/200?random=1" alt="placeholder image">
        <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm" x-text="currentSlide+1">index</button>
    </li>
    <!-- Additional slides... -->
</ul>

Step 5: Adding Navigation Controls

Navigation controls include "Previous" and "Next" buttons. These buttons are positioned absolutely within the main container:

<!-- Prev / Next Buttons -->
<div class="absolute z-50 flex justify-between w-full h-full px-4">
    <!-- Prev Button -->
    <button x-on:click="prev" class="text-6xl" :aria-disabled="atBeginning" :tabindex="atEnd ? -1 : 0">
        <!-- SVG for the left arrow -->
    </button>
    <!-- Next Button -->
    <button x-on:click="next" class="text-6xl" :aria-disabled="atEnd" :tabindex="atEnd ? -1 : 0">
        <!-- SVG for the right arrow -->
    </button>
</div>

Step 6: Adding Slide Indicators

Slide indicators are small dots that represent each slide. They provide a visual cue about the number of slides and the current slide:

<div class="absolute z-50 w-full bottom-24">
    <div class="flex justify-center space-x-2">
        <template x-for="(slide, index) in Array.from($refs.slider.children)" :key="index">
            <button @click="goToSlide(index)" :class="{'bg-gray-600': currentSlide === index, 'bg-gray-400': currentSlide !== index}" class="w-3 h-3 rounded-full hover:bg-gray-500 focus:outline-none focus:bg-gray-500"></button>
        </template>
    </div>
</div>

Full Code Example

<script src="https://unpkg.com/smoothscroll-polyfill@0.4.4/dist/smoothscroll.js"></script>

<div class="relative">

    <div x-data="{
        currentSlide: 0,
        skip: 1,
        atBeginning: false,
        atEnd: false,
        autoSlideInterval: null,
        startAutoSlide() {
            this.autoSlideInterval = setInterval(() => {
                this.next();
            }, 2500);
        },
        stopAutoSlide() {
            clearInterval(this.autoSlideInterval);
        },
        goToSlide(index) {
            let slider = this.$refs.slider;
            let offset = slider.firstElementChild.getBoundingClientRect().width;
            slider.scrollTo({ left: offset * index, behavior: 'smooth' });
        },
        next() {
            let slider = this.$refs.slider;
            let current = slider.scrollLeft;
            let offset = slider.firstElementChild.getBoundingClientRect().width;
            let maxScroll = offset * (slider.children.length - 1);

            current + offset >= maxScroll ? slider.scrollTo({ left: 0, behavior: 'smooth' }) : slider.scrollBy({ left: offset * this.skip, behavior: 'smooth' });
        },
        prev() {
            let slider = this.$refs.slider;
            let current = slider.scrollLeft;
            let offset = slider.firstElementChild.getBoundingClientRect().width;
            let maxScroll = offset * (slider.children.length - 1);

            current <= 0 ? slider.scrollTo({ left: maxScroll, behavior: 'smooth' }) : slider.scrollBy({ left: -offset * this.skip, behavior: 'smooth' });
        },
        updateButtonStates() {
            let slideEls = this.$el.parentElement.children;
            this.atBeginning = slideEls[0] === this.$el;
            this.atEnd = slideEls[slideEls.length-1] === this.$el;
        },
        focusableWhenVisible: {
            'x-intersect:enter'() { this.$el.removeAttribute('tabindex'); },
            'x-intersect:leave'() { this.$el.setAttribute('tabindex', '-1'); }
        },
        disableNextAndPreviousButtons: {
            'x-intersect:enter.threshold.05'() { this.updateButtonStates(); },
            'x-intersect:leave.threshold.05'() { this.updateButtonStates(); }
        },
        updateCurrentSlide() {
            let slider = this.$refs.slider;
            let offset = slider.firstElementChild.getBoundingClientRect().width;
            this.currentSlide = Math.round(slider.scrollLeft / offset);
        }
    }" x-init="startAutoSlide()" @mouseover="stopAutoSlide()" @mouseout="startAutoSlide()" class="flex flex-col w-full">

        <div x-on:keydown.right="next" x-on: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>

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

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

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm" x-text="currentSlide+1">index</button>
                </li>

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

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm"x-text="currentSlide+1">index</button>
                </li>

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

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm" x-text="currentSlide+1">index</button>
                </li>

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

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm" x-text="currentSlide+1">index</button>
                </li>

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

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm" x-text="currentSlide+1">index</button>
                </li>

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

                    <button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm" x-text="currentSlide+1">index</button>
                </li>
            </ul>
        </div>
        <!-- Prev / Next Buttons -->
        <div class="absolute z-10 flex justify-between w-full h-full px-4">
            <!-- Prev Button -->
            <button x-on:click="prev" class="text-6xl" :aria-disabled="atBeginning" :tabindex="atEnd ? -1 : 0">
                <span aria-hidden="true">
                    <svg xmlns="http://www.w3.org/2000/svg" class="w-auto h-5 text-gray-300 lg:h-8 hover:text-gray-400" 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>


            <!-- Next Button -->
            <button x-on:click="next" class="text-6xl" :aria-disabled="atEnd" :tabindex="atEnd ? -1 : 0">
                <span aria-hidden="true">
                    <svg xmlns="http://www.w3.org/2000/svg" class="w-auto h-5 text-gray-300 lg:h-8 hover:text-gray-400" 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>
        <!-- Indicators -->

        <div class="absolute z-10 w-full bottom-12 lg:bottom-24">
            <div class="flex justify-center space-x-2">
                <template x-for="(slide, index) in Array.from($refs.slider.children)" :key="index">
                    <button @click="goToSlide(index)" :class="{'bg-gray-500': currentSlide === index, 'bg-gray-300': currentSlide !== index}" class="w-3 h-1 rounded-full lg:w-5 hover:bg-gray-400 focus:outline-none focus:bg-gray-400"></button>
                </template>
            </div>
        </div>

    </div>
</div>

Conclusion:

With the combination of Tailwind CSS and Alpine.js, creating a responsive and interactive carousel becomes a breeze. The modular nature of both tools allows for easy customization and scalability. You can now integrate this carousel into your projects and enhance the user experience.

Victor Yoalli

This is me.