Víctor Yoalli

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:

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

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.

html
<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.
html
<div x-data="{...}" x-init="startAutoSlide()" @mouseover="stopAutoSlide()" @mouseout="startAutoSlide()" class="flex flex-col w-full">
    <!-- Carousel slides and controls go here -->
</div>

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:

html
<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:

html
<!-- 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:

html
<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

html
<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.

ESCRITOS DE VÍCTOR