Carousel
Interactive slideshow for cycling through a series of content.
TailwindCSS
Vanilla JS
Slide 1
This is the first slide
Slide 2
This is the second slide
Slide 3
This is the third slide
package showcase
import "github.com/axzilla/templui/internal/components/carousel"
templ CarouselDefault() {
@carousel.Carousel(carousel.Props{
Class: "rounded-md",
}) {
@carousel.Content() {
@carousel.Item() {
@CarouselSlide("Slide 1", "This is the first slide", "bg-blue-500")
}
@carousel.Item() {
@CarouselSlide("Slide 2", "This is the second slide", "bg-green-500")
}
@carousel.Item() {
@CarouselSlide("Slide 3", "This is the third slide", "bg-purple-500")
}
}
@carousel.Previous()
@carousel.Next()
@carousel.Indicators(carousel.IndicatorsProps{
Count: 3,
})
}
}
templ CarouselSlide(title, description, bg string) {
<div class={ bg,"w-full h-96 flex items-center justify-center text-white" }>
<div class="text-center">
<h2 class="text-3xl font-bold mb-2">{ title }</h2>
<p class="text-xl">{ description }</p>
</div>
</div>
}
Installation
templui add carousel
Copy and paste the following code into your project:
package carousel import ( "fmt" "github.com/axzilla/templui/internal/components/icon" "github.com/axzilla/templui/internal/utils" "strconv" ) type Props struct { ID string Class string Attributes templ.Attributes Autoplay bool Interval int Loop bool } type ContentProps struct { ID string Class string Attributes templ.Attributes } type ItemProps struct { ID string Class string Attributes templ.Attributes } type PreviousProps struct { ID string Class string Attributes templ.Attributes } type NextProps struct { ID string Class string Attributes templ.Attributes } type IndicatorsProps struct { ID string Class string Attributes templ.Attributes Count int } templ Carousel(props ...Props) { @Script() {{ var p Props }} if len(props) > 0 { {{ p = props[0] }} } <div if p.ID != "" { id={ p.ID } } class={ utils.TwMerge( "carousel-component relative overflow-hidden w-full", p.Class, ), } data-autoplay={ strconv.FormatBool(p.Autoplay) } data-interval={ fmt.Sprintf("%d", func() int { if p.Interval == 0 { return 5000 } return p.Interval }()) } data-loop={ strconv.FormatBool(p.Loop) } { p.Attributes... } > { children... } </div> } templ Content(props ...ContentProps) { {{ var p ContentProps }} if len(props) > 0 { {{ p = props[0] }} } <div if p.ID != "" { id={ p.ID } } class={ utils.TwMerge( "carousel-track flex h-full w-full transition-transform duration-500 ease-in-out", p.Class, ), } { p.Attributes... } > { children... } </div> } templ Item(props ...ItemProps) { {{ var p ItemProps }} if len(props) > 0 { {{ p = props[0] }} } <div if p.ID != "" { id={ p.ID } } class={ utils.TwMerge( "carousel-item flex-shrink-0 w-full h-full relative", p.Class, ), } { p.Attributes... } > { children... } </div> } templ Previous(props ...PreviousProps) { {{ var p PreviousProps }} if len(props) > 0 { {{ p = props[0] }} } <button if p.ID != "" { id={ p.ID } } class={ utils.TwMerge( "carousel-prev absolute left-2 top-1/2 transform -translate-y-1/2 p-2 rounded-full bg-black/20 text-white hover:bg-black/40 focus:outline-none", p.Class, ), } aria-label="Previous slide" type="button" { p.Attributes... } > @icon.ChevronLeft() </button> } templ Next(props ...NextProps) { {{ var p NextProps }} if len(props) > 0 { {{ p = props[0] }} } <button if p.ID != "" { id={ p.ID } } class={ utils.TwMerge( "carousel-next absolute right-2 top-1/2 transform -translate-y-1/2 p-2 rounded-full bg-black/20 text-white hover:bg-black/40 focus:outline-none", p.Class, ), } aria-label="Next slide" type="button" { p.Attributes... } > @icon.ChevronRight() </button> } templ Indicators(props ...IndicatorsProps) { {{ var p IndicatorsProps }} if len(props) > 0 { {{ p = props[0] }} } <div if p.ID != "" { id={ p.ID } } class={ utils.TwMerge( "absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2", p.Class, ), } { p.Attributes... } > for i := 0; i < p.Count; i++ { <button class={ utils.TwMerge( "carousel-indicator w-3 h-3 rounded-full bg-white/50 hover:bg-white/80 focus:outline-none transition-colors", utils.If(i == 0, "bg-white"), ), } aria-label={ fmt.Sprintf("Go to slide %d", i+1) } type="button" ></button> } </div> } var handle = templ.NewOnceHandle() templ Script() { @handle.Once() { <script defer nonce={ templ.GetNonce(ctx) }> (function() { // IIFE function initCarousel(carousel) { const track = carousel.querySelector('.carousel-track'); const items = Array.from(track?.querySelectorAll('.carousel-item') || []); if (items.length === 0) return; const indicators = Array.from(carousel.querySelectorAll('.carousel-indicator')); const prevBtn = carousel.querySelector('.carousel-prev'); const nextBtn = carousel.querySelector('.carousel-next'); const state = { currentIndex: 0, slideCount: items.length, autoplay: carousel.dataset.autoplay === 'true', interval: parseInt(carousel.dataset.interval || 5000), loop: carousel.dataset.loop === 'true', autoplayInterval: null, isHovering: false, touchStartX: 0 }; function updateTrackPosition() { track.style.transform = `translateX(-${state.currentIndex * 100}%)`; } function updateIndicators() { indicators.forEach((indicator, i) => { if (i < state.slideCount) { if (i === state.currentIndex) { indicator.classList.add('bg-white'); indicator.classList.remove('bg-white/50'); } else { indicator.classList.remove('bg-white'); indicator.classList.add('bg-white/50'); } indicator.style.display = ''; } else { indicator.style.display = 'none'; } }); } function updateButtons() { if (prevBtn) { prevBtn.disabled = !state.loop && state.currentIndex === 0; prevBtn.classList.toggle('opacity-50', prevBtn.disabled); prevBtn.classList.toggle('cursor-not-allowed', prevBtn.disabled); } if (nextBtn) { nextBtn.disabled = !state.loop && state.currentIndex === state.slideCount - 1; nextBtn.classList.toggle('opacity-50', nextBtn.disabled); nextBtn.classList.toggle('cursor-not-allowed', nextBtn.disabled); } } function startAutoplay() { if (state.autoplayInterval) { clearInterval(state.autoplayInterval); } if (state.autoplay) { state.autoplayInterval = setInterval(() => { if (!state.isHovering) { goToNext(); } }, state.interval); } } function stopAutoplay() { if (state.autoplayInterval) { clearInterval(state.autoplayInterval); state.autoplayInterval = null; } } function goToNext() { let nextIndex = state.currentIndex + 1; if (nextIndex >= state.slideCount) { if (state.loop) { nextIndex = 0; } else { return; } } goToSlide(nextIndex); } function goToPrev() { let prevIndex = state.currentIndex - 1; if (prevIndex < 0) { if (state.loop) { prevIndex = state.slideCount - 1; } else { return; } } goToSlide(prevIndex); } function goToSlide(index) { if (index < 0 || index >= state.slideCount) { if (state.loop) { index = (index + state.slideCount) % state.slideCount; } else { return; } } if (index === state.currentIndex) return; state.currentIndex = index; updateTrackPosition(); updateIndicators(); updateButtons(); if (state.autoplay && !state.isHovering) { stopAutoplay(); startAutoplay(); } } if (track) { track.addEventListener('touchstart', (e) => { state.touchStartX = e.touches[0].clientX; }, { passive: true }); track.addEventListener('touchend', (e) => { const touchEndX = e.changedTouches[0].clientX; const diff = state.touchStartX - touchEndX; const sensitivity = 50; if (Math.abs(diff) > sensitivity) { diff > 0 ? goToNext() : goToPrev(); } }, { passive: true }); } indicators.forEach((indicator, index) => { if (index < state.slideCount) { indicator.addEventListener('click', () => goToSlide(index)); } }); if (prevBtn) prevBtn.addEventListener('click', goToPrev); if (nextBtn) nextBtn.addEventListener('click', goToNext); carousel.addEventListener('mouseenter', () => { state.isHovering = true; if (state.autoplay) stopAutoplay(); }); carousel.addEventListener('mouseleave', () => { state.isHovering = false; if (state.autoplay) startAutoplay(); }); updateTrackPosition(); updateIndicators(); updateButtons(); if (state.autoplay) startAutoplay(); } function initAllComponents(root = document) { if (root instanceof Element && root.matches('.carousel-component')) { initCarousel(root); } for (const carousel of root.querySelectorAll('.carousel-component')) { initCarousel(carousel); } } const handleHtmxSwap = (event) => { const target = event.detail.elt if (target instanceof Element) { requestAnimationFrame(() => initAllComponents(target)); } }; initAllComponents(); document.addEventListener('DOMContentLoaded', () => initAllComponents()); document.body.addEventListener('htmx:afterSwap', handleHtmxSwap); document.body.addEventListener('htmx:oobAfterSwap', handleHtmxSwap); })(); // End of IIFE </script> } }
Update the import paths to match your project setup.
Examples
Autoplay
Slide 1
This is the first slide
Slide 2
This is the second slide
Slide 3
This is the third slide
package showcase
import "github.com/axzilla/templui/internal/components/carousel"
templ CarouselAutoplay() {
@carousel.Carousel(carousel.Props{
Autoplay: true,
Interval: 3000,
Loop: true,
Class: "rounded-md",
}) {
@carousel.Content() {
@carousel.Item() {
@CarouselAutoplaySlide("Slide 1", "This is the first slide", "bg-blue-500")
}
@carousel.Item() {
@CarouselAutoplaySlide("Slide 2", "This is the second slide", "bg-green-500")
}
@carousel.Item() {
@CarouselAutoplaySlide("Slide 3", "This is the third slide", "bg-purple-500")
}
}
@carousel.Previous()
@carousel.Next()
@carousel.Indicators(carousel.IndicatorsProps{
Count: 3,
})
}
}
templ CarouselAutoplaySlide(title, description, bg string) {
<div class={ bg,"w-full h-96 flex items-center justify-center text-white" }>
<div class="text-center">
<h2 class="text-3xl font-bold mb-2">{ title }</h2>
<p class="text-xl">{ description }</p>
</div>
</div>
}
Minimal
Slide 1
This is the first slide
Slide 2
This is the second slide
Slide 3
This is the third slide
package showcase
import "github.com/axzilla/templui/internal/components/carousel"
templ CarouselMinimal() {
@carousel.Carousel(carousel.Props{
Interval: 2000,
Autoplay: true,
Loop: true,
Class: "rounded-md",
}) {
@carousel.Content() {
@carousel.Item() {
@CarouselMinimalSlide("Slide 1", "This is the first slide", "bg-blue-500")
}
@carousel.Item() {
@CarouselMinimalSlide("Slide 2", "This is the second slide", "bg-green-500")
}
@carousel.Item() {
@CarouselMinimalSlide("Slide 3", "This is the third slide", "bg-purple-500")
}
}
}
}
templ CarouselMinimalSlide(title, description, bg string) {
<div class={ bg,"w-full h-96 flex items-center justify-center text-white" }>
<div class="text-center">
<h2 class="text-3xl font-bold mb-2">{ title }</h2>
<p class="text-xl">{ description }</p>
</div>
</div>
}
With Images

Nature landscape example 1

Nature landscape example 2

Nature landscape example 3
package showcase
import (
"github.com/axzilla/templui/internal/components/aspectratio"
"github.com/axzilla/templui/internal/components/carousel"
)
templ CarouselWithImages() {
@carousel.Carousel(carousel.Props{
Autoplay: true,
Interval: 5000,
Loop: true,
Class: "rounded-md overflow-hidden shadow-md",
}) {
@carousel.Content() {
@carousel.Item() {
@ImageSlide("/assets/img/demo/carousel-1.jpeg", "Image 1")
<div class="absolute bottom-0 left-0 right-0 bg-black/50 text-white p-4">
<p>Nature landscape example 1</p>
</div>
}
@carousel.Item() {
@ImageSlide("/assets/img/demo/carousel-2.jpeg", "Image 2")
<div class="absolute bottom-0 left-0 right-0 bg-black/50 text-white p-4">
<p>Nature landscape example 2</p>
</div>
}
@carousel.Item() {
@ImageSlide("/assets/img/demo/carousel-3.jpeg", "Image 3")
<div class="absolute bottom-0 left-0 right-0 bg-black/50 text-white p-4">
<p>Nature landscape example 3</p>
</div>
}
}
@carousel.Previous()
@carousel.Next()
@carousel.Indicators(carousel.IndicatorsProps{
Count: 3,
})
}
}
templ ImageSlide(src string, alt string) {
@aspectratio.AspectRatio(aspectratio.Props{
Ratio: aspectratio.RatioVideo,
}) {
<img
src={ src }
alt={ alt }
class="w-full h-full object-cover"
/>
}
}