Run the CLI
Use the CLI to add the component to your project.
npx @ngzard/ui@latest add carouselA slideshow component for cycling through elements with support for mouse drag, touch swipe, and automatic playback.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardCardComponent } from '../../card';
import { ZardCarouselModule } from '../carousel.module';
@Component({
imports: [ZardCarouselModule, ZardCardComponent],
template: `
<div class="mx-auto w-3/4 max-w-md">
<z-carousel>
<z-carousel-content>
@for (slide of slides; track slide) {
<z-carousel-item>
<z-card>
<div class="flex h-[100px] items-center justify-center text-4xl font-semibold md:h-[200px]">
{{ slide }}
</div>
</z-card>
</z-carousel-item>
}
</z-carousel-content>
</z-carousel>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCarouselDefaultComponent {
protected slides = ['1', '2', '3', '4', '5'];
}
Use the CLI to add the component to your project.
npx @ngzard/ui@latest add carouselpnpm dlx @ngzard/ui@latest add carouselyarn dlx @ngzard/ui@latest add carouselbunx @ngzard/ui@latest add carouselInstall the required dependencies for this component.
npm install embla-carousel-angular embla-carousel-autoplay embla-carousel-class-names embla-carousel-wheel-gesturespnpm add embla-carousel-angular embla-carousel-autoplay embla-carousel-class-names embla-carousel-wheel-gesturesyarn add embla-carousel-angular embla-carousel-autoplay embla-carousel-class-names embla-carousel-wheel-gesturesbun add embla-carousel-angular embla-carousel-autoplay embla-carousel-class-names embla-carousel-wheel-gesturesCreate the component directory structure and add the following files to your project.
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
input,
signal,
ViewEncapsulation,
output,
computed,
viewChild,
} from '@angular/core';
import { type ClassValue } from 'clsx';
import type { EmblaCarouselType, EmblaEventType, EmblaPluginType, EmblaOptionsType } from 'embla-carousel';
import { EmblaCarouselDirective } from 'embla-carousel-angular';
import {
carouselNextButtonVariants,
carouselPreviousButtonVariants,
carouselVariants,
type ZardCarouselControlsVariants,
type ZardCarouselOrientationVariants,
} from './carousel.variants';
import { ZardButtonComponent } from '../button/button.component';
import { ZardIconComponent } from '../icon/icon.component';
import { mergeClasses } from '@/shared/utils/merge-classes';
@Component({
selector: 'z-carousel',
imports: [CommonModule, EmblaCarouselDirective, ZardButtonComponent, ZardIconComponent],
template: `
<div class="relative">
<div
emblaCarousel
#emblaRef="emblaCarousel"
[class]="classes()"
[options]="options()"
[plugins]="zPlugins()"
[subscribeToEvents]="subscribeToEvents"
(emblaChange)="onEmblaChange($event, emblaRef.emblaApi!)"
aria-roledescription="carousel"
role="region"
tabindex="0"
>
<ng-content />
@let controls = zControls();
@if (controls === 'button') {
<ng-container *ngTemplateOutlet="buttonControls" />
} @else if (controls === 'dot') {
<ng-container *ngTemplateOutlet="dotControls" />
}
</div>
</div>
<ng-template #buttonControls>
<button
type="button"
z-button
zType="outline"
[class]="prevBtnClasses()"
[disabled]="!canScrollPrev()"
(click)="slidePrevious()"
aria-label="Previous slide"
>
<z-icon zType="chevron-left" class="size-4" />
</button>
<button
type="button"
z-button
zType="outline"
[class]="nextBtnClasses()"
[disabled]="!canScrollNext()"
(click)="slideNext()"
aria-label="Next slide"
>
<z-icon zType="chevron-right" class="size-4" />
</button>
</ng-template>
<ng-template #dotControls>
<div class="mt-2 flex justify-center gap-1">
@for (dot of dots(); track index; let index = $index) {
<span
[class]="index === selectedIndex() ? 'cursor-default' : 'cursor-pointer'"
role="button"
tabindex="0"
(click)="goTo(index)"
>
<z-icon
zType="circle-small"
[zStrokeWidth]="0"
[class]="'block size-4 ' + (index === selectedIndex() ? 'fill-primary' : 'fill-border')"
/>
</span>
}
</div>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
'(keydown.arrowleft.prevent)': 'slidePrevious()',
'(keydown.arrowright.prevent)': 'slideNext()',
},
})
export class ZardCarouselComponent {
protected readonly emblaRef = viewChild(EmblaCarouselDirective);
// Public signals and outputs
readonly class = input<ClassValue>('');
readonly zOptions = input<EmblaOptionsType>({ loop: false });
readonly zPlugins = input<EmblaPluginType[]>([]);
readonly zOrientation = input<ZardCarouselOrientationVariants>('horizontal');
readonly zControls = input<ZardCarouselControlsVariants>('button');
readonly zInited = output<EmblaCarouselType>();
readonly zSelected = output<void>();
// State signals
protected readonly selectedIndex = signal<number>(0);
protected readonly canScrollPrev = signal<boolean>(false);
protected readonly canScrollNext = signal<boolean>(false);
protected readonly scrollSnaps = signal<number[]>([]);
protected readonly subscribeToEvents: EmblaEventType[] = ['init', 'select', 'reInit'];
protected readonly options = computed(
() => ({ ...this.zOptions(), axis: this.zOrientation() === 'horizontal' ? 'x' : 'y' }) as EmblaOptionsType,
);
protected readonly dots = computed(() => new Array<string>(this.scrollSnaps().length).fill('.'));
#index = -1;
onEmblaChange(type: EmblaEventType, emblaApi: EmblaCarouselType): void {
if (type === 'init' || type === 'reInit') {
this.scrollSnaps.set(emblaApi.scrollSnapList());
this.checkNavigation(emblaApi);
if (type === 'init') {
this.zInited.emit(emblaApi);
}
return;
}
if (type === 'select' && emblaApi.selectedScrollSnap() !== this.#index) {
this.checkNavigation(emblaApi);
this.zSelected.emit();
}
}
protected slidePrevious(): void {
const emblaRef = this.emblaRef();
if (emblaRef) {
emblaRef.scrollPrev();
}
}
protected slideNext(): void {
const emblaRef = this.emblaRef();
if (emblaRef) {
emblaRef.scrollNext();
}
}
protected goTo(index: number): void {
const emblaRef = this.emblaRef();
if (emblaRef) {
emblaRef.scrollTo(index);
}
}
private checkNavigation(emblaApi: EmblaCarouselType): void {
this.#index = emblaApi.selectedScrollSnap();
this.selectedIndex.set(emblaApi.selectedScrollSnap());
this.canScrollPrev.set(emblaApi.canScrollPrev());
this.canScrollNext.set(emblaApi.canScrollNext());
}
protected readonly classes = computed(() =>
mergeClasses(carouselVariants({ zOrientation: this.zOrientation() }), this.class()),
);
protected readonly prevBtnClasses = computed(() =>
mergeClasses(carouselPreviousButtonVariants({ zOrientation: this.zOrientation() })),
);
protected readonly nextBtnClasses = computed(() =>
mergeClasses(carouselNextButtonVariants({ zOrientation: this.zOrientation() })),
);
}
import { cva, type VariantProps } from 'class-variance-authority';
export const carouselVariants = cva('overflow-hidden', {
variants: {
zOrientation: {
horizontal: '',
vertical: 'h-full',
},
zControls: {
none: '',
button: '',
dot: '',
},
},
defaultVariants: {
zOrientation: 'horizontal',
},
});
export const carouselContentVariants = cva('flex', {
variants: {
zOrientation: {
horizontal: '-ml-4 mr-0.5',
vertical: '-mt-4 flex-col',
},
},
defaultVariants: {
zOrientation: 'horizontal',
},
});
export const carouselItemVariants = cva('min-w-0 shrink-0 grow-0 basis-full', {
variants: {
zOrientation: {
horizontal: 'pl-4',
vertical: 'pt-5',
},
},
defaultVariants: {
zOrientation: 'horizontal',
},
});
export const carouselPreviousButtonVariants = cva('absolute size-8 rounded-full', {
variants: {
zOrientation: {
horizontal: 'top-1/2 -left-12.5 -translate-y-1/2',
vertical: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
},
},
defaultVariants: {
zOrientation: 'horizontal',
},
});
export const carouselNextButtonVariants = cva('absolute size-8 rounded-full', {
variants: {
zOrientation: {
horizontal: 'top-1/2 -right-12 -translate-y-1/2',
vertical: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
},
},
defaultVariants: {
zOrientation: 'horizontal',
},
});
export type ZardCarouselOrientationVariants = NonNullable<VariantProps<typeof carouselVariants>['zOrientation']>;
export type ZardCarouselControlsVariants = NonNullable<VariantProps<typeof carouselVariants>['zControls']>;
import { ChangeDetectionStrategy, Component, ViewEncapsulation, computed, inject, input } from '@angular/core';
import { type ClassValue } from 'clsx';
import { ZardCarouselComponent } from './carousel.component';
import { carouselContentVariants } from './carousel.variants';
import { mergeClasses } from '@/shared/utils/merge-classes';
@Component({
selector: 'z-carousel-content',
imports: [],
template: `
<ng-content />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
'[class]': 'classes()',
},
})
export class ZardCarouselContentComponent {
readonly #parent = inject(ZardCarouselComponent);
readonly #orientation = computed<'horizontal' | 'vertical'>(() => this.#parent.zOrientation());
readonly class = input<ClassValue>('');
protected readonly classes = computed(() =>
mergeClasses(carouselContentVariants({ zOrientation: this.#orientation() }), this.class()),
);
}
import { ChangeDetectionStrategy, Component, ViewEncapsulation, computed, inject, input } from '@angular/core';
import { type ClassValue } from 'clsx';
import { ZardCarouselComponent } from './carousel.component';
import { carouselItemVariants } from './carousel.variants';
import { mergeClasses } from '@/shared/utils/merge-classes';
@Component({
selector: 'z-carousel-item',
imports: [],
template: `
<ng-content />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
'[class]': 'classes()',
role: 'group',
'aria-roledescription': 'slide',
},
})
export class ZardCarouselItemComponent {
readonly #parent = inject(ZardCarouselComponent);
readonly #orientation = computed<'horizontal' | 'vertical'>(() => this.#parent.zOrientation());
readonly class = input<ClassValue>('');
protected readonly classes = computed(() =>
mergeClasses(carouselItemVariants({ zOrientation: this.#orientation() }), this.class()),
);
}
import { Injectable } from '@angular/core';
/**
* Service to create and manage Embla Carousel plugins
*/
@Injectable({
providedIn: 'root',
})
export class ZardCarouselPluginsService {
/**
* Creates an autoplay plugin for the carousel
*/
async createAutoplayPlugin(
options: {
delay?: number; // ms
jump?: boolean;
stopOnInteraction?: boolean;
stopOnMouseEnter?: boolean;
playOnInit?: boolean;
rootNode?: (emblaRoot: HTMLElement) => HTMLElement | null;
} = {},
) {
try {
const AutoplayModule = await import('embla-carousel-autoplay');
const Autoplay = AutoplayModule.default;
return Autoplay(options);
} catch (err) {
console.error('Error loading Autoplay plugin:', err);
throw new Error('Make sure embla-carousel-autoplay is installed.');
}
}
/**
* Helper method to create autoplay plugin with HTMLElement
* Converts HTMLElement to the function format expected by Embla
*/
async createAutoplayPluginWithElement(
options: {
delay?: number;
jump?: boolean;
stopOnInteraction?: boolean;
stopOnMouseEnter?: boolean;
playOnInit?: boolean;
rootElement?: HTMLElement;
} = {},
) {
const { rootElement, ...restOptions } = options;
const autoplayOptions = {
...restOptions,
...(rootElement && {
rootNode: () => rootElement,
}),
};
return this.createAutoplayPlugin(autoplayOptions);
}
/**
* Creates a class names plugin for the carousel
*/
async createClassNamesPlugin(
options: {
selected?: string;
dragging?: string;
draggable?: string;
} = {},
) {
try {
const ClassNamesModule = await import('embla-carousel-class-names');
const ClassNames = ClassNamesModule.default;
return ClassNames(options);
} catch (err) {
console.error('Error loading ClassNames plugin:', err);
throw new Error('Make sure embla-carousel-class-names is installed.');
}
}
/**
* Creates a wheel gestures plugin for the carousel
*/
async createWheelGesturesPlugin(
options: {
wheelDraggingClass?: string;
forceWheelAxis?: 'x' | 'y';
target?: Element;
} = {},
) {
try {
const { WheelGesturesPlugin } = await import('embla-carousel-wheel-gestures');
return WheelGesturesPlugin(options);
} catch (err) {
console.error('Error loading WheelGestures plugin:', err);
throw new Error('Make sure embla-carousel-wheel-gestures is installed.');
}
}
}
import { NgModule } from '@angular/core';
import { ZardCarouselContentComponent } from './carousel-content.component';
import { ZardCarouselItemComponent } from './carousel-item.component';
import { ZardCarouselComponent } from './carousel.component';
const CAROUSEL_COMPONENTS = [ZardCarouselComponent, ZardCarouselContentComponent, ZardCarouselItemComponent];
@NgModule({
imports: [CAROUSEL_COMPONENTS],
exports: [CAROUSEL_COMPONENTS],
})
export class ZardCarouselModule {}
export * from './carousel.component';
export * from './carousel-content.component';
export * from './carousel-item.component';
export * from './carousel-plugins.service';
export * from './carousel.variants';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardCardComponent } from '../../card';
import { ZardCarouselModule } from '../carousel.module';
@Component({
imports: [ZardCarouselModule, ZardCardComponent],
template: `
<div class="mx-auto w-3/4 max-w-md">
<z-carousel>
<z-carousel-content>
@for (slide of slides; track slide) {
<z-carousel-item>
<z-card>
<div class="flex h-[100px] items-center justify-center text-4xl font-semibold md:h-[200px]">
{{ slide }}
</div>
</z-card>
</z-carousel-item>
}
</z-carousel-content>
</z-carousel>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCarouselDefaultComponent {
protected slides = ['1', '2', '3', '4', '5'];
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardCardComponent } from '../../card';
import { ZardCarouselModule } from '../carousel.module';
@Component({
imports: [ZardCarouselModule, ZardCardComponent],
template: `
<div class="mx-auto w-3/4 max-w-md">
<z-carousel zControls="dot">
<z-carousel-content>
@for (slide of slides; track slide) {
<z-carousel-item>
<z-card>
<div class="flex h-[100px] items-center justify-center text-4xl font-semibold md:h-[200px]">
{{ slide }}
</div>
</z-card>
</z-carousel-item>
}
</z-carousel-content>
</z-carousel>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCarouselDotControlsComponent {
protected slides = ['1', '2', '3', '4', '5'];
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardCardComponent } from '../../card';
import { ZardCarouselModule } from '../carousel.module';
@Component({
imports: [ZardCarouselModule, ZardCardComponent],
template: `
<div class="mx-auto w-3/4 max-w-md">
<z-carousel zOrientation="vertical" class="w-full">
<z-carousel-content class="h-[200px] md:h-[300px]">
@for (slide of slides; track slide) {
<z-carousel-item>
<z-card class="w-full">
<div class="flex h-[100px] items-center justify-center text-4xl font-semibold md:h-[200px]">
{{ slide }}
</div>
</z-card>
</z-carousel-item>
}
</z-carousel-content>
</z-carousel>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCarouselOrientationComponent {
protected slides = ['1', '2', '3', '4', '5'];
}
To set the size of the items, you can use the basis utility class on the <z-carousel-item />.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ZardCardComponent } from '../../card';
import { ZardCarouselModule } from '../carousel.module';
@Component({
imports: [ZardCarouselModule, ZardCardComponent],
template: `
<div class="mx-auto w-3/4 max-w-md">
<z-carousel>
<z-carousel-content>
@for (slide of slides; track slide) {
<z-carousel-item class="md:basis-1/2 lg:basis-1/3">
<z-card>
<div class="flex h-[100px] items-center justify-center text-4xl font-semibold md:h-[200px]">
{{ slide }}
</div>
</z-card>
</z-carousel-item>
}
</z-carousel-content>
</z-carousel>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCarouselSizeComponent {
protected slides = ['1', '2', '3', '4', '5'];
}
To set the spacing between the items, we use a pl-[VALUE] utility on the <z-carousel-item /> and a negative -ml-[VALUE] on the <z-carousel-content />.
Content class: -ml-4
Item class: basis-1/3 pl-4
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';
import { mergeClasses } from '../../../utils/merge-classes';
import { ZardCardComponent } from '../../card';
import { ZardSegmentedComponent } from '../../segmented/segmented.component';
import { ZardCarouselModule } from '../carousel.module';
@Component({
imports: [ZardCarouselModule, ZardSegmentedComponent, ZardCardComponent],
template: `
<div class="mx-auto w-3/4 max-w-4xl">
<div class="mb-4 flex justify-center gap-2">
<z-segmented [zOptions]="options" zDefaultValue="md" (zChange)="onChange($event)" />
</div>
<z-carousel [zOptions]="{ align: 'start' }">
<z-carousel-content [class]="contentSpacingClass()">
@for (slide of slides; track slide) {
<z-carousel-item [class]="itemSpacingClass()">
<z-card>
<div class="flex h-40 items-center justify-center text-4xl font-semibold">{{ slide }}</div>
</z-card>
</z-carousel-item>
}
</z-carousel-content>
</z-carousel>
<div class="mt-4 text-center text-sm">
<p>
<strong>Content class:</strong>
{{ contentSpacingClass() }}
</p>
<p>
<strong>Item class:</strong>
{{ itemSpacingClass() }}
</p>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCarouselSpacingComponent {
protected slides = ['1', '2', '3', '4', '5', '6'];
readonly currentSpacing = signal<'sm' | 'md' | 'lg' | 'xl'>('md');
// Computed classes based on current spacing
protected readonly contentSpacingClass = computed(() => {
const spacing = this.currentSpacing();
const spacingMap = {
sm: '-ml-2',
md: '-ml-4',
lg: '-ml-6',
xl: '-ml-8',
};
return spacingMap[spacing];
});
protected readonly itemSpacingClass = computed(() => {
const spacing = this.currentSpacing();
const spacingMap = {
sm: 'pl-2',
md: 'pl-4',
lg: 'pl-6',
xl: 'pl-8',
};
return mergeClasses('basis-1/3', spacingMap[spacing]);
});
options = [
{
value: 'sm',
label: 'Small',
},
{
value: 'md',
label: 'Medium',
},
{
value: 'lg',
label: 'Large',
},
{
value: 'xl',
label: 'Extra Large',
},
];
onChange(value: string) {
this.currentSpacing.set(value as 'sm' | 'md' | 'lg' | 'xl');
}
}
Current slide: 1 / 0
Scroll progress: 0%
Can scroll prev: No
Can scroll next: No
Slides in view:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { type EmblaCarouselType } from 'embla-carousel';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardCardComponent } from '../../card';
import { ZardCarouselModule } from '../carousel.module';
@Component({
imports: [ZardCarouselModule, ZardButtonComponent, ZardCardComponent],
template: `
<div class="mx-auto w-3/4 max-w-md">
<z-carousel [zOptions]="{ loop: false }" (zSelected)="onSlideChange()" (zInited)="onCarouselInit($event)">
<z-carousel-content>
@for (slide of slides; track slide) {
<z-carousel-item>
<z-card>
<div class="flex h-40 items-center justify-center text-4xl font-semibold">{{ slide }}</div>
</z-card>
</z-carousel-item>
}
</z-carousel-content>
</z-carousel>
<div class="mt-4 flex justify-center gap-2">
<button z-button zType="outline" (click)="goToPrevious()" [disabled]="!canScrollPrev()">Previous</button>
<button z-button zType="outline" (click)="goToNext()" [disabled]="!canScrollNext()">Next</button>
<button z-button zType="outline" (click)="goToSlide(2)">Go to Slide 3</button>
</div>
<div class="mt-4 space-y-2 text-center text-sm">
<p>
<strong>Current slide:</strong>
{{ currentSlide() }} / {{ totalSlides() }}
</p>
<p>
<strong>Scroll progress:</strong>
{{ Math.round(scrollProgress() * 100) }}%
</p>
<p>
<strong>Can scroll prev:</strong>
{{ canScrollPrev() ? 'Yes' : 'No' }}
</p>
<p>
<strong>Can scroll next:</strong>
{{ canScrollNext() ? 'Yes' : 'No' }}
</p>
<p>
<strong>Slides in view:</strong>
{{ slidesInView().join(', ') }}
</p>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCarouselApiComponent {
// Math for template
Math = Math;
// Data
protected slides = ['1', '2', '3', '4', '5'];
// Carousel API signal
private readonly carouselApi = signal<EmblaCarouselType | null>(null);
// Reactive state from carousel API
protected readonly currentSlide = signal<number>(1);
protected readonly totalSlides = signal<number>(0);
protected readonly scrollProgress = signal<number>(0);
protected readonly slidesInView = signal<number[]>([]);
// Computed signals for API methods
protected readonly canScrollPrev = signal(false);
protected readonly canScrollNext = signal(false);
onCarouselInit(api: EmblaCarouselType) {
this.carouselApi.set(api);
this.update();
}
onSlideChange() {
this.update();
}
private update(): void {
const api = this.carouselApi();
if (api) {
this.scrollProgress.set(api.scrollProgress());
this.totalSlides.set(api.scrollSnapList().length);
this.currentSlide.set(api.selectedScrollSnap() + 1);
this.slidesInView.set(api.slidesInView());
this.canScrollPrev.set(api.canScrollPrev());
this.canScrollNext.set(api.canScrollNext());
}
}
goToPrevious() {
this.carouselApi()?.scrollPrev();
}
goToNext() {
this.carouselApi()?.scrollNext();
}
goToSlide(index: number) {
this.carouselApi()?.scrollTo(index);
}
}
Current slide: 1 / 0
import { ChangeDetectionStrategy, Component, type OnInit, inject, signal } from '@angular/core';
import { type EmblaCarouselType, type EmblaPluginType } from 'embla-carousel';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardCardComponent } from '../../card';
import { ZardCarouselPluginsService } from '../carousel-plugins.service';
import { ZardCarouselModule } from '../carousel.module';
@Component({
imports: [ZardCarouselModule, ZardButtonComponent, ZardCardComponent],
template: `
<div class="mx-auto w-3/4 max-w-md">
<div class="mb-4 flex gap-2">
<button type="button" z-button zType="outline" (click)="toggleAutoplay()">
{{ isAutoplayActive() ? 'Pause' : 'Start' }} Autoplay
</button>
<button type="button" z-button zType="outline" (click)="toggleLoop()">
{{ carouselOptions.loop ? 'Disable' : 'Enable' }} Loop
</button>
</div>
<z-carousel
[zOptions]="carouselOptions"
[zPlugins]="plugins"
(zInited)="onCarouselInit($event)"
(zSelected)="onSlideChange()"
>
<z-carousel-content>
@for (slide of slides; track slide) {
<z-carousel-item>
<z-card>
<div class="flex h-40 items-center justify-center text-4xl font-semibold">{{ slide }}</div>
</z-card>
</z-carousel-item>
}
</z-carousel-content>
</z-carousel>
<div class="mt-4 text-center text-sm">
<p>Current slide: {{ currentSlide() }} / {{ totalSlides() }}</p>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCarouselPluginsComponent implements OnInit {
private readonly pluginsService = inject(ZardCarouselPluginsService);
carouselOptions = {
loop: true,
align: 'center' as const,
};
plugins: EmblaPluginType[] = [];
carouselApi!: EmblaCarouselType;
protected readonly isAutoplayActive = signal(false);
protected readonly currentSlide = signal(1);
protected readonly totalSlides = signal(0);
protected slides = ['1', '2', '3', '4', '5'];
ngOnInit(): void {
// Autoplay by default
this.toggleAutoplay().catch(err => {
console.error('Failed to initialize autoplay:', err);
this.isAutoplayActive.set(false);
});
}
onCarouselInit(api: EmblaCarouselType) {
this.carouselApi = api;
this.totalSlides.set(api.scrollSnapList().length);
this.currentSlide.set(api.selectedScrollSnap() + 1);
}
protected onSlideChange(): void {
this.currentSlide.set(this.carouselApi.selectedScrollSnap() + 1);
}
async toggleAutoplay() {
this.isAutoplayActive.update(b => !b);
if (this.isAutoplayActive()) {
await this.startAutoplay();
} else {
this.pauseAutoplay();
}
this.reinitCarousel();
}
toggleLoop() {
this.carouselOptions = {
...this.carouselOptions,
loop: !this.carouselOptions.loop,
};
this.reinitCarousel();
}
private async startAutoplay() {
const autoplayPlugin = await this.pluginsService.createAutoplayPlugin({
stopOnMouseEnter: true,
delay: 2000,
stopOnInteraction: false,
});
this.plugins = [...this.plugins.filter(p => p.name !== 'autoplay'), autoplayPlugin];
}
private pauseAutoplay(): void {
this.plugins = this.plugins.filter(p => p.name !== 'autoplay');
}
private reinitCarousel() {
if (this.carouselApi) {
this.carouselApi.reInit(this.carouselOptions, this.plugins);
}
}
}