Carousel

A carousel is a slideshow used to cycle through content.

A basic carousel design that's very flexible.

            <div class="font-sans">
    <div
        x-data="{ ...carousel() }"
        x-init="init"
    >
        <div
            class="relative h-96 overflow-hidden cursor-pointer lg:h-128"
        >
            <div
                x-ref="slideContainer"
                class="absolute inset-0"
                x-on:touchstart="dragStart"
                x-on:touchmove="drag"
                x-on:touchend="dragEnd"
                x-on:mousedown="dragStart"
                x-on:mousemove="drag"
                x-on:mouseup="dragEnd"
                x-on:mouseenter="stopTimer"
                x-on:mouseleave="resetTimer"
            >
                <div
                    x-bind:class="slideClasses(0)"
                    class="z-10 absolute inset-0 transform flex items-center justify-center w-full h-full text-white bg-blue-900 transition-transform pointer-events-none"
                >
                    <div class="max-w-4xl w-full p-8 md:px-16">
                        <h3 class="text-xl font-bold sm:text-2xl md:text-3xl">Example carousel slide 1</h3>
                        <p class="mt-2">Lorem, ipsum dolor sit amet consectetur adipisicing elit. Explicabo doloremque culpa iure cumque voluptatum! Iure provident eligendi ex. Quidem atque commodi vero facilis totam maxime alias, cum quod voluptate nulla!</p>
                    </div>
                </div>
                <div
                    x-bind:class="slideClasses(1)"
                    class="absolute inset-0 transform translate-x-full flex items-center justify-center w-full h-full text-white bg-gray-900 transition-transform pointer-events-none"
                >
                    <!-- If the background image is for presentation purposes only, just use a background-image on the div above. -->
                    <img class="absolute inset-0 w-full h-full object-cover" alt="Library shelves" src="https://images.unsplash.com/photo-1498243691581-b145c3f54a5a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80&bri=-20&shad=50&high=-80"/>
                    <div class="relative max-w-4xl w-full p-8 md:px-16">
                        <h3 class="text-xl font-bold sm:text-2xl md:text-3xl">Example carousel slide 2</h3>
                        <p class="mt-2">This slide has a background image.</p>
                    </div>
                </div>
                <div
                    x-bind:class="slideClasses(2)"
                    class="absolute inset-0 transform translate-x-full flex items-center justify-center w-full h-full text-white bg-red-900 transition-transform pointer-events-none"
                >
                    <div class="max-w-4xl w-full p-8 md:px-16">
                        <h3 class="text-xl font-bold sm:text-2xl md:text-3xl">Example carousel slide 3</h3>
                        <p class="mt-2">Lorem, ipsum dolor sit amet consectetur adipisicing elit. Explicabo doloremque culpa iure cumque voluptatum! Iure provident eligendi ex.</p>
                    </div>
                </div>
            </div>
            <div class="absolute inset-y-0 left-0 flex items-center px-1 z-10 md:px-2">
                <button
                    type="button"
                    aria-label="Previous slide"
                    title="Previous slide"
                    class="inline-flex p-1 text-white bg-black bg-opacity-60 rounded md:p-2 focus:outline-none focus:ring focus:ring-blue-400"
                    x-on:click="previous"
                >
                    <svg class="w-4 h-4 md:w-6 md:h-6" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round">
                        <polyline points="15 18 9 12 15 6"></polyline>
                    </svg>
                </button>
            </div>
            <div class="absolute inset-y-0 right-0 flex items-center px-1 z-10 md:px-2">
                <button
                    type="button"
                    aria-label="Next slide"
                    title="Next slide"
                    class="inline-flex p-1 text-white bg-black bg-opacity-60 rounded md:p-2 focus:outline-none focus:ring focus:ring-blue-400"
                    x-on:click="next"
                >
                    <svg class="w-4 h-4 md:w-6 md:h-6" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round">
                        <polyline points="9 18 15 12 9 6"></polyline>
                    </svg>
                </button>
            </div>
            <div class="absolute inset-x-0 bottom-0 flex justify-center z-10">
                <div class="-mx-1 transform flex items-center justify-center">
                    <template x-if="numberOfSlides > 0" x-for="(slide, index) in numberOfSlides" x-bind:key="slide">
                        <div class="p-1">
                            <button
                                type="button"
                                x-bind:aria-label="`Go to slide ${slide}`"
                                x-bind:title="'Go to slide ' + (slide)"
                                x-bind:class="isNotActiveSlideIndicatorClasses(index)"
                                class="h-3 w-3 bg-white rounded-full transition-opacity focus:outline-none focus:ring focus:ring-blue-400"
                                x-on:click="goTo(index)"
                            ></button>
                        </div>
                    </template>
                </div>
            </div>
        </div>
    </div>

</div>

<script>
    const carousel = (delay = 5000) => {
        return {
            active: 0,
            numberOfSlides: 0,
            delay: delay,
            dragActive: false,
            timer: null,
            initialX: null,
            currentX: null,

            init() {
                this.numberOfSlides = this.$refs.slideContainer.children.length;

                this.resetTimer();
            },

            slideClasses(slideNumber) {
                return { 
                    '-translate-x-full': this.active > slideNumber, 
                    'translate-x-full': this.active < slideNumber, 
                }
            },

            isNotActiveSlideIndicatorClasses(slideNumber) {
                return { 
                    'opacity-50': this.active !== slideNumber
                }
            },

            startTimer() {
                this.timer = setInterval(() => { this.next(false) }, this.delay);
            },

            stopTimer() {
                clearInterval(this.timer);
            },

            resetTimer() {
                this.stopTimer();

                this.startTimer();
            },

            next(resetTimer = false) {
                if (resetTimer) {
                    this.resetTimer();
                }

                if (this.active === this.numberOfSlides - 1) {
                    this.active = 0;
                    return;
                }

                this.active++;
            },

            previous(resetTimer = false) {
                if (resetTimer) {
                    this.resetTimer();
                }

                if (this.active === 0) {
                    this.active = this.numberOfSlides - 1;
                    return;
                }

                this.active--;
            },

            goTo(index) {
                this.resetTimer();

                this.active = index;
            },

            dragStart(e) {
                this.dragActive = true;

                if (e.type === 'touchstart') {
                    return this.initialX = e.touches[0].clientX
                }

                this.initialX = e.clientX;
            },

            drag(e) {
                if (this.dragActive) {
                    e.preventDefault();

                    if (e.type === 'touchmove') {
                        this.currentX = e.touches[0].clientX;
                    } else {
                        this.currentX = e.clientX;
                    }
                }
            },

            dragEnd(e) {
                this.dragActive = false;

                if (this.initialX > this.currentX && this.initialX - 200 > this.currentX) {
                    return this.next(false);
                }

                if (this.initialX < this.currentX && this.initialX + 200 < this.currentX) {
                    return this.previous(false);
                }
            }
        }
    }
</script>

        

A split carousel design that can be customized to fit your content. Each side's width can be adjusted to fit more text or more image at specific breakpoints. The image and content sides can also be flipped to have the image on the left if needed.

            <div class="font-sans max-w-screen-2xl mx-auto">
    <div
        x-data="{ ...carousel() }"
        x-init="init"
    >
        <div
            class="relative h-192 overflow-hidden cursor-pointer md:h-128 lg:h-96"
        >
            <div
                x-ref="slideContainer"
                class="absolute inset-0"
                x-on:touchstart="dragStart"
                x-on:touchmove="drag"
                x-on:touchend="dragEnd"
                x-on:mousedown="dragStart"
                x-on:mousemove="drag"
                x-on:mouseup="dragEnd"
                x-on:mouseenter="stopTimer"
                x-on:mouseleave="resetTimer"
            >
                <div
                    x-bind:class="slideClasses(0)"
                    class="absolute inset-0 transform translate-x-full h-192 flex flex-wrap text-white bg-gray-900 transition-transform pointer-events-none md:h-128 lg:h-96"
                >
                    <div class="w-full h-96 flex items-center px-8 py-20 bg-black md:h-128 md:w-1/2 lg:w-2/3 lg:h-96 lg:px-16">
                        <div class="flex items-start">
                            <span class="text-10xl font-light">“</span>
                            <blockquote class="ml-4">
                                <p class="text-gray-100 text-lg font-light lg:text-xl">(If I wasn’t a professor) I think I would still be a researcher. I work closely with people at different companies or organizations where all they do is research, so I would want to do something like that.</p>
                                <cite class="mt-4 block not-italic font-light text-sm uppercase tracking-widest">Magy Seif El-Nasr</cite>
                                <p class="text-sm">Associate professor, with joint appointments in the College of Compute and Information Science and the College of Arts, Media & Design</p>
                            </blockquote>
                        </div>
                    </div>
                    <img class="w-full h-96 object-cover md:w-1/2 md:h-128 lg:w-1/3 lg:h-96" alt="Magy's portrait" src="/assets/images/portraits/magy.jpg">
                </div>
                <div
                    x-bind:class="slideClasses(1)"
                    class="absolute inset-0 transform translate-x-full h-192 flex flex-wrap text-white bg-gray-900 transition-transform pointer-events-none md:h-128 lg:h-96"
                >
                    <div class="w-full h-96 flex items-center px-8 py-20 bg-black md:h-128 md:w-1/2 lg:w-2/3 lg:h-96 lg:px-16">
                        <div class="flex items-start">
                            <span class="text-10xl font-light">“</span>
                            <blockquote class="ml-4">
                                <p class="text-gray-100 text-lg font-light lg:text-xl">I'm excited to see my daughter begin her next semester here. This job and the work i do keeps me young. That and being around young people every day. It's why I haven't aged.</p>
                                <cite class="mt-4 block not-italic font-light text-sm uppercase tracking-widest">Vincent Mitchell</cite>
                                <p class="text-sm">Facilities Service, landscape maintenance</p>
                            </blockquote>
                        </div>
                    </div>
                    <img class="w-full h-96 object-cover md:w-1/2 md:h-128 lg:w-1/3 lg:h-96" alt="Vincent's portrait" src="/assets/images/portraits/vincent.jpg">
                </div>
                <div
                    x-bind:class="slideClasses(2)"
                    class="absolute inset-0 transform translate-x-full h-192 flex flex-wrap text-white bg-gray-900 transition-transform pointer-events-none md:h-128 lg:h-96"
                >
                    <div class="w-full h-96 flex items-center px-8 py-20 bg-black md:h-128 md:w-1/2 lg:w-2/3 lg:h-96 lg:px-16">
                        <div class="flex items-start">
                            <span class="text-10xl font-light">“</span>
                            <blockquote class="ml-4">
                                <p class="text-gray-100 text-lg font-light lg:text-xl">The best part of my job is engaging with the students. They are very appreciative of the work we do. I think every year the campus gets safer. A lot of that has to do with zeroing in on areas where we can do better, and working to do that.</p>
                                <cite class="mt-4 block not-italic font-light text-sm uppercase tracking-widest">Celine Conte</cite>
                                <p class="text-sm">Community Service Officer, NUPD</p>
                            </blockquote>
                        </div>
                    </div>
                    <img class="w-full h-96 object-cover md:w-1/2 md:h-128 lg:w-1/3 lg:h-96" alt="Celine's portrait" src="/assets/images/portraits/conte.jpg">
                </div>
            </div>
            <div class="absolute left-0 bottom-0 flex justify-center p-8 z-10 md:px-16">
                <div class="-mx-1 flex items-center justify-center">
                    <template x-if="numberOfSlides > 0" x-for="(slide, index) in numberOfSlides" x-bind:key="slide">
                        <div class="p-1">
                            <button
                                x-bind:aria-label="'Go to slide ' + slide"
                                x-bind:title="'Go to slide ' + (slide + 1)"
                                type="button"
                                x-bind:class="isActiveSlideIndicatorClasses(index)"
                                class="h-4 w-4 border border-white rounded-full transition-colors focus:outline-none focus:ring focus:ring-blue-400"
                                x-on:click="goTo(index)"
                            ></button>
                        </div>
                    </template>
                </div>
            </div>
            <div class="hidden absolute bottom-0 right-0 z-10 md:flex">
                <button
                    type="button"
                    aria-label="Previous slide"
                    title="Previous slide"
                    class="relative inline-flex p-2 text-gray-700 bg-gray-200 border md:p-4 hover:bg-white focus:outline-none focus:ring focus:ring-blue-400"
                    x-on:click="previous"
                >
                    <svg class="w-6 h-6" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round">
                        <polyline points="15 18 9 12 15 6"></polyline>
                    </svg>
                </button>
                <button
                    type="button"
                    aria-label="Next slide"
                    title="Next slide"
                    class="relative inline-flex p-2 text-gray-700 bg-gray-200 border md:p-4 hover:bg-white focus:outline-none focus:ring focus:ring-blue-400"
                    x-on:click="next"
                >
                    <svg class="w-6 h-6" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round">
                        <polyline points="9 18 15 12 9 6"></polyline>
                    </svg>
                </button>
            </div>
        </div>
    </div>

</div>

<script>
    const carousel = (delay = 5000) => {
        return {
            active: 0,
            numberOfSlides: 0,
            delay: delay,
            dragActive: false,
            timer: null,
            initialX: null,
            currentX: null,

            init() {
                this.numberOfSlides = this.$refs.slideContainer.children.length;

                this.resetTimer();
            },

            slideClasses(slideNumber) {
                return { 
                    '-translate-x-full': this.active > slideNumber, 
                    'translate-x-full': this.active < slideNumber, 
                }
            },

            isActiveSlideIndicatorClasses(slideNumber) {
                return { 
                    'bg-white': this.active === slideNumber,                
                }
            },

            startTimer() {
                this.timer = setInterval(() => { this.next(false) }, this.delay);
            },

            stopTimer() {
                clearInterval(this.timer);
            },

            resetTimer() {
                this.stopTimer();

                this.startTimer();
            },

            next(resetTimer = false) {
                if (resetTimer) {
                    this.resetTimer();
                }

                if (this.active === this.numberOfSlides - 1) {
                    this.active = 0;
                    return;
                }

                this.active++;
            },

            previous(resetTimer = false) {
                if (resetTimer) {
                    this.resetTimer();
                }

                if (this.active === 0) {
                    this.active = this.numberOfSlides - 1;
                    return;
                }

                this.active--;
            },

            goTo(index) {
                this.resetTimer();

                this.active = index;
            },

            dragStart(e) {
                this.dragActive = true;

                if (e.type === 'touchstart') {
                    return this.initialX = e.touches[0].clientX
                }

                this.initialX = e.clientX;
            },

            drag(e) {
                if (this.dragActive) {
                    e.preventDefault();

                    if (e.type === 'touchmove') {
                        this.currentX = e.touches[0].clientX;
                    } else {
                        this.currentX = e.clientX;
                    }
                }
            },

            dragEnd(e) {
                this.dragActive = false;

                if (this.initialX > this.currentX && this.initialX - 200 > this.currentX) {
                    return this.next(false);
                }

                if (this.initialX < this.currentX && this.initialX + 200 < this.currentX) {
                    return this.previous(false);
                }
            }
        }
    }
</script>

        

Usage

Best to be used in place of a video. Users can scroll through the content at their own pace and auto-play options allows for users to view the content without having to click a button. Carousels load quicker than video allowing for overall faster page loads.

Poor usage includes content moving too quickly so a user cannot understand the content or no indicators that there is more content after the first slide.

Use Cases

  • When user wants to cycle through simple, quick to understand content (photo slide shows)
  • When a user wants to compare content (lab options, degree options, class options)
  • When a user wants to view a presentation (faculty spotlight with each page being a new faculty, related news articles)
  • When displaying featured stories or information (newsfeeds, featured announcements for a college page, featured research)
  • When displaying featured quotes (testimonials, quotes from library works)
  • When in conjunction with tabs to add functionality to allow a user to cycle through content either using carousel indicators or tab headlines.

Accessibility Requirements

Users should be able to pause movement, making the text easier to read for the user. All functionality, including navigating between carousel items, must be operable by the keyboard. As such, the carousel should have a pause, stop, or hide button. Changes to carousel items should be communicated to all users, including screen reader users.

The aria-label attribute on the section element is a way to label your carousel. A screen reader user will hear the label and then the role of “carousel”. Without the label, the role is not read.

Each image/slide will need to be included via the img’s src attribute. Also, include an alt attribute that describes the information you want to relay to the user for each image.

Carousels are often critiqued for poor usability, but following accessibility guidelines will improve overall usability.