Carousel

Tailwind CSS and Alpine JS Carousel

Carousels are a great way to display multiple images in a visually appealing format. They can be used to showcase products, services, and more.

Requires Alpine JS

This component requires Alpine JS v3 to function properly. Some advanced features may require additional Alpine plugins (such as focus).

Tell Me More

Default carousel

A basic carousel component with images and controls. By default, the controls on the carousel have background colors to remain visible regardless of the image, you can remove it and only keep the icons. Just make sure that the icons have sufficient contrast with the image(s).

HTML
<div x-data="{            
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',                
        },            
    ],            
    currentSlideIndex: 1,
    previous() {                
        if (this.currentSlideIndex > 1) {                    
            this.currentSlideIndex = this.currentSlideIndex - 1                
        } else {   
            // If it's the first slide, go to the last slide           
            this.currentSlideIndex = this.slides.length                
        }            
    },            
    next() {                
        if (this.currentSlideIndex < this.slides.length) {                    
            this.currentSlideIndex = this.currentSlideIndex + 1                
        } else {                 
            // If it's the last slide, go to the first slide    
            this.currentSlideIndex = 1                
        }            
    },        
}" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>
<div x-data="carousel({
    slides: [
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',
        },
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',
        },
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',
        },
    ],
})" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('carousel', (carouselData = {
            slides: [],
        },) => ({
            slides: carouselData.slides,
            currentSlideIndex: 1,
            previous() {
                if (this.currentSlideIndex > 1) {
                    this.currentSlideIndex = this.currentSlideIndex - 1
                } else {
                    // If it's the first slide, go to the last slide           
                    this.currentSlideIndex = this.slides.length
                }
            },
            next() {
                if (this.currentSlideIndex < this.slides.length) {
                    this.currentSlideIndex = this.currentSlideIndex + 1
                } else {
                    // If it's the last slide, go to the first slide    
                    this.currentSlideIndex = 1
                }
            },
        }))
    })
</script>
<div x-data="{            
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',                
        },            
    ],            
    currentSlideIndex: 1,
    previous() {                
        if (this.currentSlideIndex > 1) {                    
            this.currentSlideIndex = this.currentSlideIndex - 1                
        } else {   
            // If it's the first slide, go to the last slide           
            this.currentSlideIndex = this.slides.length                
        }            
    },            
    next() {                
        if (this.currentSlideIndex < this.slides.length) {                    
            this.currentSlideIndex = this.currentSlideIndex + 1                
        } else {                 
            // If it's the last slide, go to the first slide    
            this.currentSlideIndex = 1                
        }            
    },        
}" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>
<div x-data="{            
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',                
        },            
    ],            
    currentSlideIndex: 1,
    previous() {                
        if (this.currentSlideIndex > 1) {                    
            this.currentSlideIndex = this.currentSlideIndex - 1                
        } else {   
            // If it's the first slide, go to the last slide           
            this.currentSlideIndex = this.slides.length                
        }            
    },            
    next() {                
        if (this.currentSlideIndex < this.slides.length) {                    
            this.currentSlideIndex = this.currentSlideIndex + 1                
        } else {                 
            // If it's the last slide, go to the first slide    
            this.currentSlideIndex = 1                
        }            
    },        
}" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>

Carousel with text

A carousel component with images, title, and description.

HTML
<div x-data="{            
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',  
            title: 'Front end developers',
            description: 'The architects of the digital world, constantly battling against their mortal enemy – browser compatibility.',              
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',  
            title: 'Back end developers',
            description: 'Because not all superheroes wear capes, some wear headphones and stare at terminal screens',              
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',    
            title: 'Full stack developers',
            description: 'Where &quot;burnout&quot; is just a fancy term for &quot;Tuesday&quot;.'             
        },            
    ],            
    currentSlideIndex: 1,
    previous() {                
        if (this.currentSlideIndex > 1) {                    
            this.currentSlideIndex = this.currentSlideIndex - 1                
        } else {   
            // If it's the first slide, go to the last slide           
            this.currentSlideIndex = this.slides.length                
        }            
    },            
    next() {                
        if (this.currentSlideIndex < this.slides.length) {                    
            this.currentSlideIndex = this.currentSlideIndex + 1                
        } else {                 
            // If it's the last slide, go to the first slide    
            this.currentSlideIndex = 1                
        }            
    },        
}" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                
                <!-- Title and description -->
                <div class="">
                    <h3 class="" x-text="slide.title" x-bind:aria-describedby="'slide' + (index + 1) + 'Description'"></h3>
                    <p class="" x-text="slide.description" x-bind:id="'slide' + (index + 1) + 'Description'"></p>
                </div>

                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>
<div x-data="carousel({          
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',  
            title: 'Front end developers',
            description: 'The architects of the digital world, constantly battling against their mortal enemy – browser compatibility.',              
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',  
            title: 'Back end developers',
            description: 'Because not all superheroes wear capes, some wear headphones and stare at terminal screens',              
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',    
            title: 'Full stack developers',
            description: 'Where &quot;burnout&quot; is just a fancy term for &quot;Tuesday&quot;.'             
        },            
    ],                 
})" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                
                <!-- Title and description -->
                <div class="">
                    <h3 class="" x-text="slide.title" x-bind:aria-describedby="'slide' + (index + 1) + 'Description'"></h3>
                    <p class="" x-text="slide.description" x-bind:id="'slide' + (index + 1) + 'Description'"></p>
                </div>

                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('carousel', (carouselData = {
            slides: [],
        },) => ({
            slides: carouselData.slides,
            currentSlideIndex: 1,
            previous() {
                if (this.currentSlideIndex > 1) {
                    this.currentSlideIndex = this.currentSlideIndex - 1
                } else {
                    // If it's the first slide, go to the last slide           
                    this.currentSlideIndex = this.slides.length
                }
            },
            next() {
                if (this.currentSlideIndex < this.slides.length) {
                    this.currentSlideIndex = this.currentSlideIndex + 1
                } else {
                    // If it's the last slide, go to the first slide    
                    this.currentSlideIndex = 1
                }
            },
        }))
    })
</script>

Carousel with CTA button

A carousel component with images, title, description, and a call-to-action button.

HTML
<div x-data="{            
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',  
            title: 'Front end developers',
            description: 'The architects of the digital world, constantly battling against their mortal enemy – browser compatibility.',    
            ctaUrl: 'https://example.com',
            ctaText: 'Become a Developer',          
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',  
            title: 'Back end developers',
            description: 'Because not all superheroes wear capes, some wear headphones and stare at terminal screens', 
            ctaUrl: 'https://example.com',
            ctaText: 'Become a Developer',             
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',    
            title: 'Full stack developers',
            description: 'Where &quot;burnout&quot; is just a fancy term for &quot;Tuesday&quot;.',
            ctaUrl: 'https://example.com',
            ctaText: 'Become a Developer',          
        },            
    ],            
    currentSlideIndex: 1,
    previous() {                
        if (this.currentSlideIndex > 1) {                    
            this.currentSlideIndex = this.currentSlideIndex - 1                
        } else {   
            // If it's the first slide, go to the last slide           
            this.currentSlideIndex = this.slides.length                
        }            
    },            
    next() {                
        if (this.currentSlideIndex < this.slides.length) {                    
            this.currentSlideIndex = this.currentSlideIndex + 1                
        } else {                 
            // If it's the last slide, go to the first slide    
            this.currentSlideIndex = 1                
        }            
    },        
}" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                
                <!-- Title and description -->
                <div class="">
                    <h3 class="" x-text="slide.title" x-bind:aria-describedby="'slide' + (index + 1) + 'Description'"></h3>
                    <p class="" x-text="slide.description" x-bind:id="'slide' + (index + 1) + 'Description'"></p>
                    <button type="button" x-cloak x-show="slide.ctaUrl !== null" class="" x-text="slide.ctaText"></button>
                </div>

                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>
<div x-data="carousel({          
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',  
            title: 'Front end developers',
            description: 'The architects of the digital world, constantly battling against their mortal enemy – browser compatibility.',    
            ctaUrl: 'https://example.com',
            ctaText: 'Become a Developer',          
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',  
            title: 'Back end developers',
            description: 'Because not all superheroes wear capes, some wear headphones and stare at terminal screens', 
            ctaUrl: 'https://example.com',
            ctaText: 'Become a Developer',             
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',    
            title: 'Full stack developers',
            description: 'Where &quot;burnout&quot; is just a fancy term for &quot;Tuesday&quot;.',
            ctaUrl: 'https://example.com',
            ctaText: 'Become a Developer',          
        },       
    ],                 
})" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                
                <!-- Title and description -->
                <div class="">
                    <h3 class="" x-text="slide.title" x-bind:aria-describedby="'slide' + (index + 1) + 'Description'"></h3>
                    <p class="" x-text="slide.description" x-bind:id="'slide' + (index + 1) + 'Description'"></p>
                    <button type="button" x-cloak x-show="slide.ctaUrl !== null" class="" x-text="slide.ctaText"></button>
                </div>

                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('carousel', (carouselData = {
            slides: [],
        },) => ({
            slides: carouselData.slides,
            currentSlideIndex: 1,
            previous() {
                if (this.currentSlideIndex > 1) {
                    this.currentSlideIndex = this.currentSlideIndex - 1
                } else {
                    // If it's the first slide, go to the last slide           
                    this.currentSlideIndex = this.slides.length
                }
            },
            next() {
                if (this.currentSlideIndex < this.slides.length) {
                    this.currentSlideIndex = this.currentSlideIndex + 1
                } else {
                    // If it's the last slide, go to the first slide    
                    this.currentSlideIndex = 1
                }
            },
        }))
    })
</script>

Carousel with Autoplay

A carousel component with autoplay functionality. You can adjust the time interval between each slide. Additionally, there is a pause/play button that can be used to stop or start the carousel's autoplay.

HTML
<div x-data="{            
    // Sets the time between each slides in milliseconds
    autoplayIntervalTime: ,
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',  
            title: 'Front end developers',
            description: 'The architects of the digital world, constantly battling against their mortal enemy – browser compatibility.',           
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',  
            title: 'Back end developers',
            description: 'Because not all superheroes wear capes, some wear headphones and stare at terminal screens',            
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',    
            title: 'Full stack developers',
            description: 'Where &quot;burnout&quot; is just a fancy term for &quot;Tuesday&quot;.',       
        },            
    ],            
    currentSlideIndex: 1,
    isPaused: false,
    autoplayInterval: null,
    previous() {                
        if (this.currentSlideIndex > 1) {                    
            this.currentSlideIndex = this.currentSlideIndex - 1                
        } else {   
            // If it's the first slide, go to the last slide           
            this.currentSlideIndex = this.slides.length                
        }            
    },            
    next() {                
        if (this.currentSlideIndex < this.slides.length) {                    
            this.currentSlideIndex = this.currentSlideIndex + 1                
        } else {                 
            // If it's the last slide, go to the first slide    
            this.currentSlideIndex = 1                
        }            
    },    
    autoplay() {
        this.autoplayInterval = setInterval(() => {
            if (! this.isPaused) {
                this.next()
            }
        }, this.autoplayIntervalTime)
    },
    // Updates interval time   
    setAutoplayInterval(newIntervalTime) {
        clearInterval(this.autoplayInterval)
        this.autoplayIntervalTime = newIntervalTime
        this.autoplay()
    },    
}" x-init="autoplay" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                
                <!-- Title and description -->
                <div class="">
                    <h3 class="" x-text="slide.title" x-bind:aria-describedby="'slide' + (index + 1) + 'Description'"></h3>
                    <p class="" x-text="slide.description" x-bind:id="'slide' + (index + 1) + 'Description'"></p>
                </div>

                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>
<div x-data="carousel({          
    // Sets the time between each slides in milliseconds
    intervalTime: ,
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',  
            title: 'Front end developers',
            description: 'The architects of the digital world, constantly battling against their mortal enemy – browser compatibility.',    
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',  
            title: 'Back end developers',
            description: 'Because not all superheroes wear capes, some wear headphones and stare at terminal screens',         
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',    
            title: 'Full stack developers',
            description: 'Where &quot;burnout&quot; is just a fancy term for &quot;Tuesday&quot;.',
        },            
    ],             
})" x-init="autoplay" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.1000ms>
                
                <!-- Title and description -->
                <div class="">
                    <h3 class="" x-text="slide.title" x-bind:aria-describedby="'slide' + (index + 1) + 'Description'"></h3>
                    <p class="" x-text="slide.description" x-bind:id="'slide' + (index + 1) + 'Description'"></p>
                </div>

                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>
    
<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('carousel', (carouselData = {
            slides: [],
            intervalTime: 0,
        },) => ({
            slides: carouselData.slides,
            autoplayIntervalTime: carouselData.intervalTime,
            currentSlideIndex: 1,
            isPaused: false,
            autoplayInterval: null,
            previous() {
                if (this.currentSlideIndex > 1) {
                    this.currentSlideIndex = this.currentSlideIndex - 1
                } else {
                    // If it's the first slide, go to the last slide           
                    this.currentSlideIndex = this.slides.length
                }
            },
            next() {
                if (this.currentSlideIndex < this.slides.length) {
                    this.currentSlideIndex = this.currentSlideIndex + 1
                } else {
                    // If it's the last slide, go to the first slide    
                    this.currentSlideIndex = 1
                }
            },
            autoplay() {
                this.autoplayInterval = setInterval(() => {
                    if (! this.isPaused) {
                        this.next()
                    }
                }, this.autoplayIntervalTime)
            },
            // Updates interval time   
            setAutoplayIntervalTime(newIntervalTime) {
                clearInterval(this.autoplayInterval)
                this.autoplayIntervalTime = newIntervalTime
                this.autoplay()
            },
        }))
    })
</script>

Carousel with fixed aspect ratio

An image slider carousel with a fixed aspect ratio, perfect for displaying images without cropping. Ensure the carousel's aspect ratio matches that of your image(s).

HTML
<div x-data="{            
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/aspect-slide-1.webp',
            imgAlt: 'New collection - ride the wave of excitement.',
        },
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/aspect-slide-2.webp',
            imgAlt: 'Up to 30% discount, gear up for adventure.',
        },

        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/aspect-slide-3.webp',
            imgAlt: '30% off all surfing essentials, ride like a pro.',
        },    
    ],            
    currentSlideIndex: 1,
    previous() {                
        if (this.currentSlideIndex > 1) {                    
            this.currentSlideIndex = this.currentSlideIndex - 1                
        } else {   
            // If it's the first slide, go to the last slide           
            this.currentSlideIndex = this.slides.length                
        }            
    },            
    next() {                
        if (this.currentSlideIndex < this.slides.length) {                    
            this.currentSlideIndex = this.currentSlideIndex + 1                
        } else {                 
            // If it's the last slide, go to the first slide    
            this.currentSlideIndex = 1                
        }            
    },        
}" class="">
   
    <!-- slides -->
    <!-- Change aspect-[3/1] to match your images aspect ratio -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.700ms>
                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>
<div x-data="carousel({        
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/aspect-slide-1.webp',
            imgAlt: 'New collection - ride the wave of excitement.',
        },
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/aspect-slide-2.webp',
            imgAlt: 'Up to 30% discount, gear up for adventure.',
        },

        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/aspect-slide-3.webp',
            imgAlt: '30% off all surfing essentials, ride like a pro.',
        },    
    ],                  
})" class="">
   
    <!-- slides -->
    <!-- Change aspect-[3/1] to match your images aspect ratio -->
    <div class="">
        <template x-for="(slide, index) in slides">
            <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.700ms>
                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('carousel', (carouselData = {
            slides: [],
        },) => ({
            slides: carouselData.slides,
            currentSlideIndex: 1,
            previous() {
                if (this.currentSlideIndex > 1) {
                    this.currentSlideIndex = this.currentSlideIndex - 1
                } else {
                    // If it's the first slide, go to the last slide           
                    this.currentSlideIndex = this.slides.length
                }
            },
            next() {
                if (this.currentSlideIndex < this.slides.length) {
                    this.currentSlideIndex = this.currentSlideIndex + 1
                } else {
                    // If it's the last slide, go to the first slide    
                    this.currentSlideIndex = 1
                }
            },
        }))
    })
</script>

Carousel on a card

A carousel component with images and controls inside a card. Idea for ecommerce websites when you need to display multiple images of a product on a single card.

BioHazardApe #343

price0.45 ETH

by @apeMakers

BioHazardApe NFT showcases a captivating collection of digital artworks inspired by the wild essence of apes, each piece a gem in the jungle of digital art.

HTML
<article class="" style="-webkit-mask-image: -webkit-radial-gradient(white, black)">
    <!-- Carousel -->
    <div class=""> 
        <div x-data="{            
            slides: [                
                {
                    imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/nft-1.webp',
                    imgAlt: 'An illustration of a cyberpunk-style ape wearing a hoodie and futuristic headphones.',
                },
                {
                    imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/nft-2.webp',
                    imgAlt: 'An illustration of a cyberpunk-style ape wearing ajacket.',
                },

                {
                    imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/nft-3.webp',
                    imgAlt: 'An illustration of a cyberpunk-style ape wearing a cape with chains on the neck.',
                },
            ],            
            currentSlideIndex: 1,
            previous() {                
                if (this.currentSlideIndex > 1) {                    
                    this.currentSlideIndex = this.currentSlideIndex - 1                
                } else {   
                    // If it's the first slide, go to the last slide           
                    this.currentSlideIndex = this.slides.length                
                }            
            },            
            next() {                
                if (this.currentSlideIndex < this.slides.length) {                    
                    this.currentSlideIndex = this.currentSlideIndex + 1                
                } else {                 
                    // If it's the last slide, go to the first slide    
                    this.currentSlideIndex = 1                
                }            
            },        
        }" class="">
           
            <!-- slides -->
            <div class="">
                <template x-for="(slide, index) in slides">
                    <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.300ms>
                        <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
                    </div>
                </template>
            </div>
            
        </div>
    </div>
    <!-- Content -->
    <div class="">
        <!-- Header -->
        <div class="">
            <!-- Title -->
            <div class="">
                <h3 class="" aria-describedby="nftDescription">BioHazardApe #343</h3>
            </div>
            <!-- Price -->
            <span class=""><span class="">price</span>0.45 ETH</span>
        </div>
        <p id="nftDescription" class="">
            by <a href="#" class="">@apeMakers</a>
            <br/>
            <br/>
            BioHazardApe NFT showcases a captivating collection of digital artworks inspired by the wild essence of apes, each piece a gem in the jungle of digital art.
        </p>
        <!-- Button -->
        <button type="button" class="">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="">
                <path fill-rule="evenodd" d="M5 4a3 3 0 0 1 6 0v1h.643a1.5 1.5 0 0 1 1.492 1.35l.7 7A1.5 1.5 0 0 1 12.342 15H3.657a1.5 1.5 0 0 1-1.492-1.65l.7-7A1.5 1.5 0 0 1 4.357 5H5V4Zm4.5 0v1h-3V4a1.5 1.5 0 0 1 3 0Zm-3 3.75a.75.75 0 0 0-1.5 0v1a3 3 0 1 0 6 0v-1a.75.75 0 0 0-1.5 0v1a1.5 1.5 0 1 1-3 0v-1Z" clip-rule="evenodd" />
            </svg>
            I Must Have It
        </button>
    </div>
</article>
<article class="" style="-webkit-mask-image: -webkit-radial-gradient(white, black)">
    <!-- Carousel -->
    <div class=""> 
        <div x-data="carousel({        
            slides: [                
                {
                    imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/nft-1.webp',
                    imgAlt: 'An illustration of a cyberpunk-style ape wearing a hoodie and futuristic headphones.',
                },
                {
                    imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/nft-2.webp',
                    imgAlt: 'An illustration of a cyberpunk-style ape wearing ajacket.',
                },

                {
                    imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/nft-3.webp',
                    imgAlt: 'An illustration of a cyberpunk-style ape wearing a cape with chains on the neck.',
                },
            ],                  
        })" class="">
           
            <!-- slides -->
            <div class="">
                <template x-for="(slide, index) in slides">
                    <div x-cloak x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.300ms>
                        <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
                    </div>
                </template>
            </div>
            
        </div>
    </div>
    <!-- Content -->
    <div class="">
        <!-- Header -->
        <div class="">
            <!-- Title -->
            <div class="">
                <h3 class="" aria-describedby="nftDescription">BioHazardApe #343</h3>
            </div>
            <!-- Price -->
            <span class=""><span class="">price</span>0.45 ETH</span>
        </div>
        <p id="nftDescription" class="">
            by <a href="#" class="">@apeMakers</a>
            <br/>
            <br/>
            BioHazardApe NFT showcases a captivating collection of digital artworks inspired by the wild essence of apes, each piece a gem in the jungle of digital art.
        </p>
        <!-- Button -->
        <button type="button" class="">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="">
                <path fill-rule="evenodd" d="M5 4a3 3 0 0 1 6 0v1h.643a1.5 1.5 0 0 1 1.492 1.35l.7 7A1.5 1.5 0 0 1 12.342 15H3.657a1.5 1.5 0 0 1-1.492-1.65l.7-7A1.5 1.5 0 0 1 4.357 5H5V4Zm4.5 0v1h-3V4a1.5 1.5 0 0 1 3 0Zm-3 3.75a.75.75 0 0 0-1.5 0v1a3 3 0 1 0 6 0v-1a.75.75 0 0 0-1.5 0v1a1.5 1.5 0 1 1-3 0v-1Z" clip-rule="evenodd" />
            </svg>
            I Must Have It
        </button>
    </div>
</article>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('carousel', (carouselData = {
            slides: [],
        },) => ({
            slides: carouselData.slides,
            currentSlideIndex: 1,
            previous() {
                if (this.currentSlideIndex > 1) {
                    this.currentSlideIndex = this.currentSlideIndex - 1
                } else {
                    // If it's the first slide, go to the last slide           
                    this.currentSlideIndex = this.slides.length
                }
            },
            next() {
                if (this.currentSlideIndex < this.slides.length) {
                    this.currentSlideIndex = this.currentSlideIndex + 1
                } else {
                    // If it's the last slide, go to the first slide    
                    this.currentSlideIndex = 1
                }
            },
        }))
    })
</script>

Carousel with touch

A carousel with touch swipe functionality.

HTML
<div x-data="{            
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',                
        },            
    ],            
    currentSlideIndex: 1,
    touchStartX: null,
    touchEndX: null,
    swipeThreshold: 50,
    previous() {                
        if (this.currentSlideIndex > 1) {                    
            this.currentSlideIndex = this.currentSlideIndex - 1                
        } else {   
            // If it's the first slide, go to the last slide           
            this.currentSlideIndex = this.slides.length                
        }            
    },            
    next() {                
        if (this.currentSlideIndex < this.slides.length) {                    
            this.currentSlideIndex = this.currentSlideIndex + 1                
        } else {                 
            // If it's the last slide, go to the first slide    
            this.currentSlideIndex = 1                
        }            
    },        
    handleTouchStart(event) {
        this.touchStartX = event.touches[0].clientX
    },
    handleTouchMove(event) {
        this.touchEndX = event.touches[0].clientX
    },
    handleTouchEnd() {
        if(this.touchEndX){
            if (this.touchStartX - this.touchEndX > this.swipeThreshold) {
                this.next()
            }
            if (this.touchStartX - this.touchEndX < -this.swipeThreshold) {
                this.previous()
            }
            this.touchStartX = null
            this.touchEndX = null
        }
    },     
}" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="" x-on:touchstart="handleTouchStart($event)" x-on:touchmove="handleTouchMove($event)" x-on:touchend="handleTouchEnd()">
        <template x-for="(slide, index) in slides">
            <div x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.700ms>
                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>
<div x-data="carousel({            
    slides: [                
        {
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-1.webp',
            imgAlt: 'Vibrant abstract painting with swirling blue and light pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-2.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling red, yellow, and pink hues on a canvas.',                
        },                
        {                    
            imgSrc: 'https://penguinui.s3.amazonaws.com/component-assets/carousel/default-slide-3.webp',                    
            imgAlt: 'Vibrant abstract painting with swirling blue and purple hues on a canvas.',                
        },            
    ],            
})" class="">
   
    <!-- slides -->
    <!-- Change min-h-[50svh] to your preferred height size -->
    <div class="" x-on:touchstart="handleTouchStart($event)" x-on:touchmove="handleTouchMove($event)" x-on:touchend="handleTouchEnd()">
        <template x-for="(slide, index) in slides">
            <div x-show="currentSlideIndex == index + 1" class="" x-transition.opacity.duration.700ms>
                <img class="" x-bind:src="slide.imgSrc" x-bind:alt="slide.imgAlt" />
            </div>
        </template>
    </div>
    
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('carousel', (carouselData = {
            slides: [],
        },) => ({
            slides: carouselData.slides,
            currentSlideIndex: 1,
            touchStartX: null,
            touchEndX: null,
            swipeThreshold: 50,
            previous() {
                if (this.currentSlideIndex > 1) {
                    this.currentSlideIndex = this.currentSlideIndex - 1
                } else {
                    // If it's the first slide, go to the last slide           
                    this.currentSlideIndex = this.slides.length
                }
            },
            next() {
                if (this.currentSlideIndex < this.slides.length) {
                    this.currentSlideIndex = this.currentSlideIndex + 1
                } else {
                    // If it's the last slide, go to the first slide    
                    this.currentSlideIndex = 1
                }
            },
            handleTouchStart(event) {
                this.touchStartX = event.touches[0].clientX
            },
            handleTouchMove(event) {
                this.touchEndX = event.touches[0].clientX
            },
            handleTouchEnd() {
                if (this.touchEndX){
                    if (this.touchStartX - this.touchEndX > this.swipeThreshold) {
                        this.next()
                    }
                    if (this.touchStartX - this.touchEndX < -this.swipeThreshold) {
                        this.previous()
                    }
                    this.touchStartX = null
                    this.touchEndX = null
                }
            },     
        }))
    })
</script>

Data

List of all Alpine JS data used in this component.

Property Description
slides Array - List of data for each slide
currentSlideIndex Number - Index of the currently displayed slide
next() Function - Moves the carousel to the next slide. If the current slide is the last slide, it then moves to the first slide
previous() Function - Moves the carousel to the previous slide. If the current slide is the first slide, it then moves to the last slide
For autoplay carousel component
autoplayIntervalTime Number - The time, in milliseconds, that the carousel waits on each slide before automatically moving to the next one
isPaused Boolean - Autoplay carousel is paused/playing
autoplayInterval Number - Interval ID, a unique identifier representing the interval that was set
autoplay() Function - Starts the carousel autoplay
setAutoplayInterval()
param:newIntervalTime
Function - Updates the autoplay interval with a new time value (newIntervalTime) specified in milliseconds
For touch carousel component
touchStartX Number - The X coordinate on the screen where a touch event was started
touchEndX Number - The X coordinate on the screen where a touch event ended
swipeThreshold Number - The minimum amount of difference, in pixels, between the starting point and end point of a touch swipe on the screen that will trigger a slide change
handleTouchStart() Function - Retrieves the x-coordinate of touch points when a touch event begins
handleTouchMove() Function - Retrieves the x-coordinate of touch points when the X touch point moves
handleTouchEnd() Function - Determines the x-coordinate of touch points at the end of a touch event and invokes either the next() or previous() functions based on the swipe direction (left or right)

Keyboard Navigation

Key Action
Tab Next focusable element on the carousel gets the focus
Space Enter Focused item on the carousel gets selected
* Disclaimer: Some of the images and copy text on this page are generated by artificial intelligence and are only used as placeholders for examples. They may contain errors, inconsistencies, or outdated information.