Carousel
Embla-powered slide carousel wired through data attributes.
A carousel without writing a single line of carousel code.
Zazz Carousel is a thin wrapper around Embla Carousel, the same engine shadcn/ui's carousel is built on. The library auto-initializes every [data-embla="root"] on the page, reads its options from data attributes, and binds the prev, next, and dot controls for you. No per-carousel JavaScript.
Use it for image sliders, testimonial scrollers, logo marquees, and any other "swipe through a list" pattern.
Anatomy
<div data-embla="root" data-embla-loop="true">
<div data-embla="viewport">
<div data-embla="container">
<div data-embla="slide">Slide 1</div>
<div data-embla="slide">Slide 2</div>
<div data-embla="slide">Slide 3</div>
</div>
</div>
<button data-embla="prev" aria-label="Previous slide">Prev</button>
<button data-embla="next" aria-label="Next slide">Next</button>
<div data-embla="dots">
<button data-embla="dot" aria-label="Go to slide"></button>
</div>
</div>| Part | Attribute | Notes |
|---|---|---|
| Root | data-embla="root" | The carousel container. Holds all configuration attributes. |
| Viewport | data-embla="viewport" | The visible window. Required. The library binds Embla to this element. |
| Container | data-embla="container" | The flex track that holds all slides. |
| Slide | data-embla="slide" | One slide. Repeat for each item. |
| Prev | data-embla="prev" | Optional. The library wires a click handler that calls scrollPrev(). |
| Next | data-embla="next" | Optional. The library wires a click handler that calls scrollNext(). |
| Dots | data-embla="dots" | Optional. Container for the dot pagination. |
| Dot | data-embla="dot" | Template dot. The library clones it once per slide and binds click-to-scroll. |
The viewport is the only required descendant. Everything else is opt-in.
Configuration
Every Embla option is settable via a data attribute on the root. The library converts data-embla-<key>="<value>" into { <key>: <value> } and passes it to Embla.
<div
data-embla="root"
data-embla-loop="true"
data-embla-align="start"
data-embla-skipsnaps="false"
data-embla-draginteraction="true"
>
<!-- viewport, slides, controls -->
</div>See the Embla options reference for the full list.
Plugins
Three Embla plugins are bundled and opt-in via attribute prefix. The plugin loads only when at least one of its attributes is present.
| Plugin | Attribute prefix | Reference |
|---|---|---|
| Autoplay | data-embla-autoplay-* | Autoplay options |
| Auto Scroll | data-embla-autoscroll-* | Auto Scroll options |
| Class Names | data-embla-classnames-* | Class Names options |
<div
data-embla="root"
data-embla-loop="true"
data-embla-autoplay-delay="3000"
data-embla-autoplay-stoponinteraction="false"
>
<!-- viewport, slides -->
</div>Autoplay and Auto Scroll are mutually exclusive in practice. Pick one per carousel.
Dot pagination
The dot pattern uses a single template dot that the library clones once per slide. Author one [data-embla="dot"] inside [data-embla="dots"]; the library handles the rest.
<div data-embla="dots">
<button data-embla="dot" class="carousel__dot" aria-label="Go to slide"></button>
</div>When the carousel initializes:
- The library reads
emblaApi.scrollSnapList().lengthto determine how many dots to render. - The template dot is cloned that many times. Existing children of
[data-embla="dots"]are cleared first. - Each clone gets a click handler that calls
emblaApi.scrollTo(i). - The dot matching the current slide receives the
.is-activeclass. The active class updates on everyselectevent.
If the carousel has one slide or zero, no dots are rendered.
Style the dots however the design calls for. The library only manages presence, click handlers, and the active class.
Multiple carousels per page
The library queries every [data-embla="root"] on the page and initializes each independently. Configuration is per-root, so two carousels in the same document can have different options:
<div data-embla="root" data-embla-loop="true" data-embla-autoplay-delay="4000">
<!-- hero slider -->
</div>
<div data-embla="root" data-embla-align="start" data-embla-classnames-snapped="is-snapped">
<!-- logo scroller -->
</div>Dependencies
The library expects the Embla globals to be on window before it runs:
EmblaCarouselEmblaCarouselAutoplay(only if autoplay attributes are used)EmblaCarouselAutoScroll(only if autoscroll attributes are used)EmblaCarouselClassNames(only if classnames attributes are used)
In Webflow, load the Embla scripts via project-level custom code before the Zazz carousel script. In a vanilla setup, include them in the same order:
<script src="https://unpkg.com/embla-carousel/embla-carousel.umd.js"></script>
<script src="https://unpkg.com/embla-carousel-autoplay/embla-carousel-autoplay.umd.js"></script>
<script src="https://unpkg.com/embla-carousel-class-names/embla-carousel-class-names.umd.js"></script>
<!-- Zazz carousel script -->Only include the plugin bundles you actually use.
Accessibility
- Prev and next buttons need an
aria-labelsince they are typically icon-only. - Each dot button should have an
aria-label(e.g., "Go to slide 2"). The library does not set this; author it on the template dot or update it during clone. - The carousel itself is a region of related content. Wrap it in a
<section aria-roledescription="carousel" aria-label="...">when the surrounding context doesn't already describe it. - Embla handles keyboard focus on the viewport and respects
prefers-reduced-motionfor its scroll animations.
Cross-platform
| Platform | Reference |
|---|---|
| Figma | Not applicable. Carousels are a runtime behavior. |
| Webflow | Drop the script into global-end__carousels (or any page-level custom code block before </body>). Author markup using the data-embla-* attributes on standard Webflow divs. |
| CSS / Tailwind | Load the Embla scripts and the Zazz carousel script in order. The same data-embla-* markup works without changes. |