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/components"
templ CarouselDefault() {
@components.Carousel(components.CarouselProps{
Class: "rounded-md",
}) {
@components.CarouselContent() {
@components.CarouselItem() {
@CarouselSlide("Slide 1", "This is the first slide", "bg-blue-500")
}
@components.CarouselItem() {
@CarouselSlide("Slide 2", "This is the second slide", "bg-green-500")
}
@components.CarouselItem() {
@CarouselSlide("Slide 3", "This is the third slide", "bg-purple-500")
}
}
@components.CarouselPrevious()
@components.CarouselNext()
@components.CarouselIndicators(components.CarouselIndicatorsProps{
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>
}
package components
import (
"fmt"
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"strconv"
)
type CarouselProps struct {
ID string
Class string
Attributes templ.Attributes
Autoplay bool
Interval int
Loop bool
}
type CarouselContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselPreviousProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselNextProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselIndicatorsProps struct {
ID string
Class string
Attributes templ.Attributes
Count int
}
templ Carousel(props ...CarouselProps) {
{{ var p CarouselProps }}
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 CarouselContent(props ...CarouselContentProps) {
{{ var p CarouselContentProps }}
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 CarouselItem(props ...CarouselItemProps) {
{{ var p CarouselItemProps }}
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 CarouselPrevious(props ...CarouselPreviousProps) {
{{ var p CarouselPreviousProps }}
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... }
>
@icons.ChevronLeft()
</button>
}
templ CarouselNext(props ...CarouselNextProps) {
{{ var p CarouselNextProps }}
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... }
>
@icons.ChevronRight()
</button>
}
templ CarouselIndicators(props ...CarouselIndicatorsProps) {
{{ var p CarouselIndicatorsProps }}
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>
}
templ CarouselScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
function initCarousel(carouselEl) {
const state = {
currentIndex: 0,
slideCount: 0,
autoplay: carouselEl.dataset.autoplay === 'true',
interval: parseInt(carouselEl.dataset.interval || 5000),
loop: carouselEl.dataset.loop === 'true',
autoplayInterval: null,
isHovering: false,
touchStartX: 0
};
const track = carouselEl.querySelector('.carousel-track');
const items = Array.from(track ? track.querySelectorAll('.carousel-item') : []);
const indicators = Array.from(carouselEl.querySelectorAll('.carousel-indicator'));
const prevBtn = carouselEl.querySelector('.carousel-prev');
const nextBtn = carouselEl.querySelector('.carousel-next');
// Set slide count dynamically based on DOM
state.slideCount = items.length;
function updateTrackPosition() {
if (track) {
track.style.transform = `translateX(-${state.currentIndex * 100}%)`;
}
}
function updateIndicators() {
indicators.forEach((indicator, i) => {
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');
}
});
}
function updateButtons() {
if (prevBtn) {
prevBtn.disabled = !state.loop && state.currentIndex === 0;
if (prevBtn.disabled) {
prevBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
prevBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
if (nextBtn) {
nextBtn.disabled = !state.loop && state.currentIndex === state.slideCount - 1;
if (nextBtn.disabled) {
nextBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
nextBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
}
function startAutoplay() {
if (state.autoplayInterval) {
clearInterval(state.autoplayInterval);
}
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 {
nextIndex = state.slideCount - 1;
}
}
goToSlide(nextIndex);
}
function goToPrev() {
let prevIndex = state.currentIndex - 1;
if (prevIndex < 0) {
if (state.loop) {
prevIndex = state.slideCount - 1;
} else {
prevIndex = 0;
}
}
goToSlide(prevIndex);
}
function goToSlide(index) {
if (index === state.currentIndex) return;
state.currentIndex = index;
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) {
stopAutoplay();
if (!state.isHovering) {
startAutoplay();
}
}
}
function handleTouchStart(event) {
state.touchStartX = event.touches[0].clientX;
}
function handleTouchEnd(event) {
const touchEndX = event.changedTouches[0].clientX;
const diff = state.touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
goToNext();
} else {
goToPrev();
}
}
}
if (track) {
track.addEventListener('touchstart', handleTouchStart);
track.addEventListener('touchend', handleTouchEnd);
}
indicators.forEach((indicator, index) => {
indicator.addEventListener('click', () => goToSlide(index));
});
if (prevBtn) {
prevBtn.addEventListener('click', goToPrev);
}
if (nextBtn) {
nextBtn.addEventListener('click', goToNext);
}
carouselEl.addEventListener('mouseenter', () => {
state.isHovering = true;
if (state.autoplay) {
stopAutoplay();
}
});
carouselEl.addEventListener('mouseleave', () => {
state.isHovering = false;
if (state.autoplay) {
startAutoplay();
}
});
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) {
startAutoplay();
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.carousel-component').forEach(carousel => {
initCarousel(carousel);
});
});
</script>
}
}
Usage
1. Add the script to your page/layout:
// Option A: All components (recommended)
@utils.ComponentScripts()
// Option B: Just Carousel
@components.CarouselScript()
2. Use the component:
@components.Carousel(components.CarouselProps{...})
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/components"
templ CarouselAutoplay() {
@components.Carousel(components.CarouselProps{
Autoplay: true,
Interval: 3000,
Loop: true,
Class: "rounded-md",
}) {
@components.CarouselContent() {
@components.CarouselItem() {
@CarouselAutoplaySlide("Slide 1", "This is the first slide", "bg-blue-500")
}
@components.CarouselItem() {
@CarouselAutoplaySlide("Slide 2", "This is the second slide", "bg-green-500")
}
@components.CarouselItem() {
@CarouselAutoplaySlide("Slide 3", "This is the third slide", "bg-purple-500")
}
}
@components.CarouselPrevious()
@components.CarouselNext()
@components.CarouselIndicators(components.CarouselIndicatorsProps{
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>
}
package components
import (
"fmt"
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"strconv"
)
type CarouselProps struct {
ID string
Class string
Attributes templ.Attributes
Autoplay bool
Interval int
Loop bool
}
type CarouselContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselPreviousProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselNextProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselIndicatorsProps struct {
ID string
Class string
Attributes templ.Attributes
Count int
}
templ Carousel(props ...CarouselProps) {
{{ var p CarouselProps }}
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 CarouselContent(props ...CarouselContentProps) {
{{ var p CarouselContentProps }}
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 CarouselItem(props ...CarouselItemProps) {
{{ var p CarouselItemProps }}
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 CarouselPrevious(props ...CarouselPreviousProps) {
{{ var p CarouselPreviousProps }}
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... }
>
@icons.ChevronLeft()
</button>
}
templ CarouselNext(props ...CarouselNextProps) {
{{ var p CarouselNextProps }}
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... }
>
@icons.ChevronRight()
</button>
}
templ CarouselIndicators(props ...CarouselIndicatorsProps) {
{{ var p CarouselIndicatorsProps }}
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>
}
templ CarouselScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
function initCarousel(carouselEl) {
const state = {
currentIndex: 0,
slideCount: 0,
autoplay: carouselEl.dataset.autoplay === 'true',
interval: parseInt(carouselEl.dataset.interval || 5000),
loop: carouselEl.dataset.loop === 'true',
autoplayInterval: null,
isHovering: false,
touchStartX: 0
};
const track = carouselEl.querySelector('.carousel-track');
const items = Array.from(track ? track.querySelectorAll('.carousel-item') : []);
const indicators = Array.from(carouselEl.querySelectorAll('.carousel-indicator'));
const prevBtn = carouselEl.querySelector('.carousel-prev');
const nextBtn = carouselEl.querySelector('.carousel-next');
// Set slide count dynamically based on DOM
state.slideCount = items.length;
function updateTrackPosition() {
if (track) {
track.style.transform = `translateX(-${state.currentIndex * 100}%)`;
}
}
function updateIndicators() {
indicators.forEach((indicator, i) => {
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');
}
});
}
function updateButtons() {
if (prevBtn) {
prevBtn.disabled = !state.loop && state.currentIndex === 0;
if (prevBtn.disabled) {
prevBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
prevBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
if (nextBtn) {
nextBtn.disabled = !state.loop && state.currentIndex === state.slideCount - 1;
if (nextBtn.disabled) {
nextBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
nextBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
}
function startAutoplay() {
if (state.autoplayInterval) {
clearInterval(state.autoplayInterval);
}
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 {
nextIndex = state.slideCount - 1;
}
}
goToSlide(nextIndex);
}
function goToPrev() {
let prevIndex = state.currentIndex - 1;
if (prevIndex < 0) {
if (state.loop) {
prevIndex = state.slideCount - 1;
} else {
prevIndex = 0;
}
}
goToSlide(prevIndex);
}
function goToSlide(index) {
if (index === state.currentIndex) return;
state.currentIndex = index;
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) {
stopAutoplay();
if (!state.isHovering) {
startAutoplay();
}
}
}
function handleTouchStart(event) {
state.touchStartX = event.touches[0].clientX;
}
function handleTouchEnd(event) {
const touchEndX = event.changedTouches[0].clientX;
const diff = state.touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
goToNext();
} else {
goToPrev();
}
}
}
if (track) {
track.addEventListener('touchstart', handleTouchStart);
track.addEventListener('touchend', handleTouchEnd);
}
indicators.forEach((indicator, index) => {
indicator.addEventListener('click', () => goToSlide(index));
});
if (prevBtn) {
prevBtn.addEventListener('click', goToPrev);
}
if (nextBtn) {
nextBtn.addEventListener('click', goToNext);
}
carouselEl.addEventListener('mouseenter', () => {
state.isHovering = true;
if (state.autoplay) {
stopAutoplay();
}
});
carouselEl.addEventListener('mouseleave', () => {
state.isHovering = false;
if (state.autoplay) {
startAutoplay();
}
});
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) {
startAutoplay();
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.carousel-component').forEach(carousel => {
initCarousel(carousel);
});
});
</script>
}
}
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/components"
templ CarouselMinimal() {
@components.Carousel(components.CarouselProps{
Interval: 2000,
Autoplay: true,
Loop: true,
Class: "rounded-md",
}) {
@components.CarouselContent() {
@components.CarouselItem() {
@CarouselMinimalSlide("Slide 1", "This is the first slide", "bg-blue-500")
}
@components.CarouselItem() {
@CarouselMinimalSlide("Slide 2", "This is the second slide", "bg-green-500")
}
@components.CarouselItem() {
@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>
}
package components
import (
"fmt"
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"strconv"
)
type CarouselProps struct {
ID string
Class string
Attributes templ.Attributes
Autoplay bool
Interval int
Loop bool
}
type CarouselContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselPreviousProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselNextProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselIndicatorsProps struct {
ID string
Class string
Attributes templ.Attributes
Count int
}
templ Carousel(props ...CarouselProps) {
{{ var p CarouselProps }}
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 CarouselContent(props ...CarouselContentProps) {
{{ var p CarouselContentProps }}
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 CarouselItem(props ...CarouselItemProps) {
{{ var p CarouselItemProps }}
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 CarouselPrevious(props ...CarouselPreviousProps) {
{{ var p CarouselPreviousProps }}
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... }
>
@icons.ChevronLeft()
</button>
}
templ CarouselNext(props ...CarouselNextProps) {
{{ var p CarouselNextProps }}
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... }
>
@icons.ChevronRight()
</button>
}
templ CarouselIndicators(props ...CarouselIndicatorsProps) {
{{ var p CarouselIndicatorsProps }}
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>
}
templ CarouselScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
function initCarousel(carouselEl) {
const state = {
currentIndex: 0,
slideCount: 0,
autoplay: carouselEl.dataset.autoplay === 'true',
interval: parseInt(carouselEl.dataset.interval || 5000),
loop: carouselEl.dataset.loop === 'true',
autoplayInterval: null,
isHovering: false,
touchStartX: 0
};
const track = carouselEl.querySelector('.carousel-track');
const items = Array.from(track ? track.querySelectorAll('.carousel-item') : []);
const indicators = Array.from(carouselEl.querySelectorAll('.carousel-indicator'));
const prevBtn = carouselEl.querySelector('.carousel-prev');
const nextBtn = carouselEl.querySelector('.carousel-next');
// Set slide count dynamically based on DOM
state.slideCount = items.length;
function updateTrackPosition() {
if (track) {
track.style.transform = `translateX(-${state.currentIndex * 100}%)`;
}
}
function updateIndicators() {
indicators.forEach((indicator, i) => {
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');
}
});
}
function updateButtons() {
if (prevBtn) {
prevBtn.disabled = !state.loop && state.currentIndex === 0;
if (prevBtn.disabled) {
prevBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
prevBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
if (nextBtn) {
nextBtn.disabled = !state.loop && state.currentIndex === state.slideCount - 1;
if (nextBtn.disabled) {
nextBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
nextBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
}
function startAutoplay() {
if (state.autoplayInterval) {
clearInterval(state.autoplayInterval);
}
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 {
nextIndex = state.slideCount - 1;
}
}
goToSlide(nextIndex);
}
function goToPrev() {
let prevIndex = state.currentIndex - 1;
if (prevIndex < 0) {
if (state.loop) {
prevIndex = state.slideCount - 1;
} else {
prevIndex = 0;
}
}
goToSlide(prevIndex);
}
function goToSlide(index) {
if (index === state.currentIndex) return;
state.currentIndex = index;
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) {
stopAutoplay();
if (!state.isHovering) {
startAutoplay();
}
}
}
function handleTouchStart(event) {
state.touchStartX = event.touches[0].clientX;
}
function handleTouchEnd(event) {
const touchEndX = event.changedTouches[0].clientX;
const diff = state.touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
goToNext();
} else {
goToPrev();
}
}
}
if (track) {
track.addEventListener('touchstart', handleTouchStart);
track.addEventListener('touchend', handleTouchEnd);
}
indicators.forEach((indicator, index) => {
indicator.addEventListener('click', () => goToSlide(index));
});
if (prevBtn) {
prevBtn.addEventListener('click', goToPrev);
}
if (nextBtn) {
nextBtn.addEventListener('click', goToNext);
}
carouselEl.addEventListener('mouseenter', () => {
state.isHovering = true;
if (state.autoplay) {
stopAutoplay();
}
});
carouselEl.addEventListener('mouseleave', () => {
state.isHovering = false;
if (state.autoplay) {
startAutoplay();
}
});
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) {
startAutoplay();
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.carousel-component').forEach(carousel => {
initCarousel(carousel);
});
});
</script>
}
}
With Images

Nature landscape example 1

Nature landscape example 2

Nature landscape example 3
package showcase
import "github.com/axzilla/templui/components"
templ CarouselWithImages() {
@components.Carousel(components.CarouselProps{
Autoplay: true,
Interval: 5000,
Loop: true,
Class: "rounded-md overflow-hidden shadow-md",
}) {
@components.CarouselContent() {
@components.CarouselItem() {
@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>
}
@components.CarouselItem() {
@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>
}
@components.CarouselItem() {
@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>
}
}
@components.CarouselPrevious()
@components.CarouselNext()
@components.CarouselIndicators(components.CarouselIndicatorsProps{
Count: 3,
})
}
}
templ ImageSlide(src string, alt string) {
@components.AspectRatio(components.AspectRatioProps{
Ratio: components.AspectRatioVideo,
}) {
<img
src={ src }
alt={ alt }
class="w-full h-full object-cover"
/>
}
}
package components
import (
"fmt"
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"strconv"
)
type CarouselProps struct {
ID string
Class string
Attributes templ.Attributes
Autoplay bool
Interval int
Loop bool
}
type CarouselContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselPreviousProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselNextProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CarouselIndicatorsProps struct {
ID string
Class string
Attributes templ.Attributes
Count int
}
templ Carousel(props ...CarouselProps) {
{{ var p CarouselProps }}
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 CarouselContent(props ...CarouselContentProps) {
{{ var p CarouselContentProps }}
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 CarouselItem(props ...CarouselItemProps) {
{{ var p CarouselItemProps }}
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 CarouselPrevious(props ...CarouselPreviousProps) {
{{ var p CarouselPreviousProps }}
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... }
>
@icons.ChevronLeft()
</button>
}
templ CarouselNext(props ...CarouselNextProps) {
{{ var p CarouselNextProps }}
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... }
>
@icons.ChevronRight()
</button>
}
templ CarouselIndicators(props ...CarouselIndicatorsProps) {
{{ var p CarouselIndicatorsProps }}
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>
}
templ CarouselScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
function initCarousel(carouselEl) {
const state = {
currentIndex: 0,
slideCount: 0,
autoplay: carouselEl.dataset.autoplay === 'true',
interval: parseInt(carouselEl.dataset.interval || 5000),
loop: carouselEl.dataset.loop === 'true',
autoplayInterval: null,
isHovering: false,
touchStartX: 0
};
const track = carouselEl.querySelector('.carousel-track');
const items = Array.from(track ? track.querySelectorAll('.carousel-item') : []);
const indicators = Array.from(carouselEl.querySelectorAll('.carousel-indicator'));
const prevBtn = carouselEl.querySelector('.carousel-prev');
const nextBtn = carouselEl.querySelector('.carousel-next');
// Set slide count dynamically based on DOM
state.slideCount = items.length;
function updateTrackPosition() {
if (track) {
track.style.transform = `translateX(-${state.currentIndex * 100}%)`;
}
}
function updateIndicators() {
indicators.forEach((indicator, i) => {
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');
}
});
}
function updateButtons() {
if (prevBtn) {
prevBtn.disabled = !state.loop && state.currentIndex === 0;
if (prevBtn.disabled) {
prevBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
prevBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
if (nextBtn) {
nextBtn.disabled = !state.loop && state.currentIndex === state.slideCount - 1;
if (nextBtn.disabled) {
nextBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
nextBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
}
function startAutoplay() {
if (state.autoplayInterval) {
clearInterval(state.autoplayInterval);
}
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 {
nextIndex = state.slideCount - 1;
}
}
goToSlide(nextIndex);
}
function goToPrev() {
let prevIndex = state.currentIndex - 1;
if (prevIndex < 0) {
if (state.loop) {
prevIndex = state.slideCount - 1;
} else {
prevIndex = 0;
}
}
goToSlide(prevIndex);
}
function goToSlide(index) {
if (index === state.currentIndex) return;
state.currentIndex = index;
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) {
stopAutoplay();
if (!state.isHovering) {
startAutoplay();
}
}
}
function handleTouchStart(event) {
state.touchStartX = event.touches[0].clientX;
}
function handleTouchEnd(event) {
const touchEndX = event.changedTouches[0].clientX;
const diff = state.touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
goToNext();
} else {
goToPrev();
}
}
}
if (track) {
track.addEventListener('touchstart', handleTouchStart);
track.addEventListener('touchend', handleTouchEnd);
}
indicators.forEach((indicator, index) => {
indicator.addEventListener('click', () => goToSlide(index));
});
if (prevBtn) {
prevBtn.addEventListener('click', goToPrev);
}
if (nextBtn) {
nextBtn.addEventListener('click', goToNext);
}
carouselEl.addEventListener('mouseenter', () => {
state.isHovering = true;
if (state.autoplay) {
stopAutoplay();
}
});
carouselEl.addEventListener('mouseleave', () => {
state.isHovering = false;
if (state.autoplay) {
startAutoplay();
}
});
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) {
startAutoplay();
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.carousel-component').forEach(carousel => {
initCarousel(carousel);
});
});
</script>
}
}