An Angular wrapper for Embla Carousel.
npm i embla-carousel embla-carousel-angularscrollTo,scrollPrev,scrollNextwere renamed togoTo,goToPrev,goToNext.EmblaEventTypevalues are now lowercase (pointerdown,slidesinview,reinit, ...).initevent is removed.
Embla Carousel provides the handy EmblaCarouselDirective standalone directive for seamless integration with Angular. A minimal setup requires an overflow wrapper and a scroll container. Start by adding the following structure to your carousel:
import { Component, effect, viewChild } from '@angular/core'
import {
EmblaCarouselDirective,
EmblaCarouselType
} from 'embla-carousel-angular'
@Component({
selector: 'app-carousel',
template: `
<div class="embla" emblaCarousel [options]="options">
<div class="embla__container">
<div class="embla__slide">Slide 1</div>
<div class="embla__slide">Slide 2</div>
<div class="embla__slide">Slide 3</div>
</div>
</div>
`,
imports: [EmblaCarouselDirective],
standalone: true
})
export class CarouselComponent implements AfterViewInit {
private emblaRef = viewChild<EmblaCarouselDirective>(EmblaCarouselDirective);
private emblaApi?: EmblaCarouselType
private options = { loop: false }
constructor() {
effect(() => {
this.emblaApi = this.emblaRef()?.emblaApi;
});
}
}The element with the classname embla is needed to cover the scroll overflow. Its child element with the container classname is the scroll body that scrolls the slides. Continue by adding the following CSS to these elements:
.embla {
overflow: hidden;
}
.embla__container {
display: flex;
}
.embla__slide {
flex: 0 0 100%;
min-width: 0;
}The emblaCarousel directive takes the Embla Carousel options as part of its inputs. Additionally, you can access the API by using the viewChild signal to access the carousel in the effect.
Warning
Calling the following embla APIs directly will trigger too much ChangeDetection, which will lead to serious performance issues.
emblaApi.on()emblaApi.goToNext()emblaApi.goToPrev()emblaApi.goTo()
Consider using the following methods which are wrapped with ngZone.runOutsideAngular():
EmblaCarouselDirective.goToPrev()EmblaCarouselDirective.goToNext()EmblaCarouselDirective.goTo()
import { Component, effect, viewChild } from '@angular/core'
import {
EmblaCarouselDirective,
EmblaCarouselType
} from 'embla-carousel-angular'
@Component({
selector: 'app-carousel',
template: `
<div class="embla" emblaCarousel [options]="options">
<div class="embla__container">
<div class="embla__slide">Slide 1</div>
<div class="embla__slide">Slide 2</div>
<div class="embla__slide">Slide 3</div>
</div>
</div>
`,
imports: [EmblaCarouselDirective],
standalone: true
})
export class CarouselComponent {
private emblaRef = viewChild<EmblaCarouselDirective>(EmblaCarouselDirective);
private emblaApi?: EmblaCarouselType
private options = { loop: false }
constructor() {
effect(() => {
this.emblaApi = this.emblaRef()?.emblaApi;
});
}
}The emblaCarousel directive also provides a custom event: emblaChange that forwards embla events, also wrapped in ngZone.runOutsideAngular. You need to listen by passing the specified event names into subscribeToEvents input on demand.
import { Component, effect, viewChild } from '@angular/core'
import {
EmblaCarouselDirective,
EmblaCarouselType,
EmblaEventType
} from 'embla-carousel-angular'
@Component({
selector: 'app-carousel',
template: `
<div
class="embla"
emblaCarousel
[options]="options"
[subscribeToEvents]="subscribeToEvents"
(emblaChange)="onEmblaChange($event)"
>
<div class="embla__container">
<div class="embla__slide">Slide 1</div>
<div class="embla__slide">Slide 2</div>
<div class="embla__slide">Slide 3</div>
</div>
</div>
`,
imports: [EmblaCarouselDirective],
standalone: true
})
export class CarouselComponent {
private emblaRef = viewChild<EmblaCarouselDirective>(EmblaCarouselDirective);
private emblaApi?: EmblaCarouselType
private options = { loop: false }
constructor() {
effect(() => {
this.emblaApi = this.emblaRef()?.emblaApi;
});
}
public readonly subscribeToEvents: EmblaEventType[] = [
'pointerdown',
'pointerup',
'slideschanged',
'slidesinview',
'select',
'settle',
'destroy',
'reinit',
'resize',
'scroll'
]
onEmblaChange(event: EmblaEventType): void {
console.log(`Embla event triggered: ${event}`)
}
}When using Angular Universal / SSR:
- The directive only creates Embla in the browser, so
emblaApiisundefinedon the server. - Guard API calls with optional chaining or browser-only effects.
- Use Embla v9
options.ssrto pre-compute snap positions for better first paint stability. - After client init,
emblaApi?.ssrStyles(containerSelector, slideSelector)can be used to generate inline styles for SSR/hydration workflows if you need deterministic server/client layout.
Start by installing the plugin you want to use. In this example, we're going to install the Autoplay plugin:
npm install embla-carousel-autoplay --saveThe emblaCarousel directive inputs also accepts plugins. Note that plugins need to be passed in an array like so:
import { Component, effect, viewChild } from '@angular/core'
import {
EmblaCarouselDirective,
EmblaCarouselType
} from 'embla-carousel-angular'
import Autoplay from 'embla-carousel-autoplay'
@Component({
selector: 'app-carousel',
template: `
<div class="embla" emblaCarousel [options]="options" [plugins]="plugins">
<div class="embla__container">
<div class="embla__slide">Slide 1</div>
<div class="embla__slide">Slide 2</div>
<div class="embla__slide">Slide 3</div>
</div>
</div>
`,
imports: [EmblaCarouselDirective],
standalone: true
})
export class CarouselComponent {
private emblaRef = viewChild<EmblaCarouselDirective>(EmblaCarouselDirective);
private emblaApi?: EmblaCarouselType
public options = { loop: false }
public plugins = [Autoplay()]
constructor() {
effect(() => {
this.emblaApi = this.emblaRef()?.emblaApi;
});
}
}Thanks to davidjerleke, zip-fa and JeanMeche for the review and advice.