Carousel
an accessible carousel component that leverages native CSS Scroll Snap for smooth, performant scrolling between slides.
Features
- Uses native CSS Scroll Snap
- Supports horizontal and vertical orientations
- Supports alignment of slides (start, center or end alignment)
- Show multiple slides at a time
- Supports looping and auto-playing
- Supports custom spacing between slides
Installation
To use the carousel machine in your project, run the following command in your command line:
npm install @zag-js/carousel @zag-js/react # or yarn add @zag-js/carousel @zag-js/react
npm install @zag-js/carousel @zag-js/solid # or yarn add @zag-js/carousel @zag-js/solid
npm install @zag-js/carousel @zag-js/vue # or yarn add @zag-js/carousel @zag-js/vue
npm install @zag-js/carousel @zag-js/svelte # or yarn add @zag-js/carousel @zag-js/svelte
Anatomy
To set up the carousel correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
First, import the carousel package into your project
import * as carousel from "@zag-js/carousel"
The carousel package exports two key functions:
machine— The state machine logic for the carousel widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
You'll also need to provide a unique
idto theuseMachinehook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the carousel machine in your project 🔥
Note: The carousel requires that you provide a
slideCountproperty in the machine's context. This is the number of slides in the carousel.
import * as carousel from "@zag-js/carousel" import { normalizeProps, useMachine } from "@zag-js/react" const items = [ "https://tinyurl.com/5b6ka8jd", "https://tinyurl.com/7rmccdn5", "https://tinyurl.com/59jxz9uu", ] export function Carousel() { const service = useMachine(carousel.machine, { id: "1", slideCount: items.length, }) const api = carousel.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <div {...api.getControlProps()}> <button {...api.getPrevTriggerProps()}>Prev</button> <button {...api.getNextTriggerProps()}>Next</button> </div> <div {...api.getItemGroupProps()}> {items.map((image, index) => ( <div {...api.getItemProps({ index })} key={index}> <img src={image} alt="" style={{ height: "300px", width: "100%", objectFit: "cover" }} /> </div> ))} </div> <div {...api.getIndicatorGroupProps()}> {api.pageSnapPoints.map((_, index) => ( <button {...api.getIndicatorProps({ index })} key={index} /> ))} </div> </div> ) }
import * as carousel from "@zag-js/carousel" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, For } from "solid-js" const items = [ "https://tinyurl.com/5b6ka8jd", "https://tinyurl.com/7rmccdn5", "https://tinyurl.com/59jxz9uu", ] export function Carousel() { const service = useMachine(carousel.machine, { id: createUniqueId(), slideCount: items.length, }) const api = createMemo(() => carousel.connect(service, normalizeProps)) return ( <main class="carousel"> <div {...api().getRootProps()}> <div {...api().getControlProps()}> <button {...api().getPrevTriggerProps()}>Prev</button> <button {...api().getNextTriggerProps()}>Next</button> </div> <div {...api().getItemGroupProps()}> <Index each={items}> {(image, index) => ( <div {...api().getItemProps({ index })}> <img src={image()} alt="" /> </div> )} </Index> </div> <div {...api().getIndicatorGroupProps()}> <Index each={api().pageSnapPoints}> {(_, index) => <button {...api().getIndicatorProps({ index })} />} </Index> </div> </div> </main> ) }
<script setup> import * as carousel from "@zag-js/carousel" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const items = [ "https://tinyurl.com/5b6ka8jd", "https://tinyurl.com/7rmccdn5", "https://tinyurl.com/59jxz9uu", ] const service = useMachine(carousel.machine, { id: "1", slideCount: items.length, }) const api = computed(() => carousel.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <div v-bind="api.getControlProps()"> <button v-bind="api.getPrevTriggerProps()">Prev</button> <button v-bind="api.getNextTriggerProps()">Next</button> </div> <div v-bind="api.getItemGroupProps()"> <div v-for="(image, index) in items" :key="index" v-bind="api.getItemProps({ index })" > <img :src="image" alt="" /> </div> </div> <div v-bind="api.getIndicatorGroupProps()"> <button v-for="(_, index) in api.pageSnapPoints" :key="index" v-bind="api.getIndicatorProps({ index })" ></button> </div> </div> </template>
<script lang="ts"> import * as carousel from "@zag-js/carousel" import { normalizeProps, useMachine } from "@zag-js/svelte" const items = [ "https://tinyurl.com/5b6ka8jd", "https://tinyurl.com/7rmccdn5", "https://tinyurl.com/59jxz9uu", ] const id = $props.id() const service = useMachine( carousel.machine, ({ id: id, slideCount: items.length }), ) const api = $derived(carousel.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <div {...api.getControlProps()}> <button {...api.getPrevTriggerProps()}>Prev</button> <button {...api.getNextTriggerProps()}>Next</button> </div> <div {...api.getItemGroupProps()}> {#each items as image, index} <div {...api.getItemProps({ index })}> <img src={image} alt="" /> </div> {/each} </div> <div {...api.getIndicatorGroupProps()}> {#each api.pageSnapPoints as _, index} <!-- svelte-ignore a11y_consider_explicit_label --> <button {...api.getIndicatorProps({ index })}></button> {/each} </div> </div>
Vertical carousel
To create a vertical carousel, set the orientation property in the machine's
context to vertical.
const service = useMachine(carousel.machine, { orientation: "vertical", })
Setting the initial slide
To set the initial slide of the carousel, pass the defaultPage property to the
machine's context.
The
defaultPagecorresponds to the scroll snap position index based on the layout. It does not necessarily correspond to the index of the slide in the carousel.
const service = useMachine(carousel.machine, { defaultPage: 2, })
Setting the number of slides to show at a time
To customize number of slides to show at a time, set the slidesPerPage
property in the machine's context. The value must be an integer.
const service = useMachine(carousel.machine, { slidesPerPage: 2, })
Setting the number of slides to move at a time
To customize number of slides to move at a time, set the slidesPerMove
property in the machine's context. The value must be an integer or auto.
const service = useMachine(carousel.machine, { slidesPerMove: 2, })
Considerations
- If the value is
auto, the carousel will move the number of slides equal to the number of slides per page. - Ensure the
slidesPerMoveis less than or equal to theslidesPerPageto avoid skipping slides. - On touch devices,
slidesPerMoveis not enforced during active swiping. The browser's native scrolling and CSS Scroll Snap determine slide movement for optimal performance and UX.
Setting the carousel should loop around
To allow looping of slides, set the loop property in the machine's context to
true.
const service = useMachine(carousel.machine, { loop: true, })
Setting the gap between slides
To customize spacing between slides, set the spacing property in the machine's
context to a valid CSS unit.
const service = useMachine(carousel.machine, { spacing: "16px", })
Listening for page changes
When the carousel page changes, the onPageChange callback is invoked.
const service = useMachine(carousel.machine, { onPageChange(details) { // details => { page: number } console.log("selected page:", details.page) }, })
Dragging the carousel
To allow dragging the carousel with the mouse, set the allowMouseDrag property
in the machine's context to true.
const service = useMachine(carousel.machine, { allowMouseDrag: true, })
Autoplaying the carousel
To allow the carousel to autoplay, set the autoplay property in the machine's
context to true.
const service = useMachine(carousel.machine, { autoplay: true, })
Alternatively, you can configure the autoplay interval by setting the delay
property in the machine's context.
const service = useMachine(carousel.machine, { autoplay: { delay: 2000 }, })
Styling guide
Earlier, we mentioned that each carousel part has a data-part attribute added
to them to select and style them in the DOM.
[data-part="root"] { /* styles for the root part */ } [data-part="item-group"] { /* styles for the item-group part */ } [data-part="item"] { /* styles for the root part */ } [data-part="control"] { /* styles for the control part */ } [data-part="next-trigger"] { /* styles for the next-trigger part */ } [data-part="prev-trigger"] { /* styles for the prev-trigger part */ } [data-part="indicator-group"] { /* styles for the indicator-group part */ } [data-part="indicator"] { /* styles for the indicator part */ } [data-part="autoplay-trigger"] { /* styles for the autoplay-trigger part */ }
Active state
When a carousel's indicator is active, a data-current attribute is set on the
indicator.
[data-part="indicator"][data-current] { /* styles for the indicator's active state */ }
Methods and Properties
The carousel's api exposes the following methods and properties:
Machine Context
The carousel machine exposes the following context properties:
idsPartial<{ root: string; item: (index: number) => string; itemGroup: string; nextTrigger: string; prevTrigger: string; indicatorGroup: string; indicator: (index: number) => string; }>The ids of the elements in the carousel. Useful for composition.translationsIntlTranslationsThe localized messages to use.slidesPerPagenumberThe number of slides to show at a time.slidesPerMovenumber | "auto"The number of slides to scroll at a time. When set to `auto`, the number of slides to scroll is determined by the `slidesPerPage` property.autoplayboolean | { delay: number; }Whether to scroll automatically. The default delay is 4000ms.allowMouseDragbooleanWhether to allow scrolling via dragging with mouseloopbooleanWhether the carousel should loop around.pagenumberThe controlled page of the carousel.defaultPagenumberThe initial page to scroll to when rendered. Use when you don't need to control the page of the carousel.spacingstringThe amount of space between items.paddingstringDefines the extra space added around the scrollable area, enabling nearby items to remain partially in view.onPageChange(details: PageChangeDetails) => voidFunction called when the page changes.inViewThresholdnumber | number[]The threshold for determining if an item is in view.snapType"proximity" | "mandatory"The snap type of the item.slideCountnumberThe total number of slides. Useful for SSR to render the initial ating the snap points.onDragStatusChange(details: DragStatusDetails) => voidFunction called when the drag status changes.onAutoplayStatusChange(details: AutoplayStatusDetails) => voidFunction called when the autoplay status changes.dir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.orientation"horizontal" | "vertical"The orientation of the element.
Machine API
The carousel api exposes the following methods:
pagenumberThe current index of the carouselpageSnapPointsnumber[]The current snap points of the carouselisPlayingbooleanWhether the carousel is auto playingisDraggingbooleanWhether the carousel is being dragged. This only works when `draggable` is true.canScrollNextbooleanWhether the carousel is can scroll to the next viewcanScrollPrevbooleanWhether the carousel is can scroll to the previous viewscrollToIndex(index: number, instant?: boolean) => voidFunction to scroll to a specific item indexscrollTo(page: number, instant?: boolean) => voidFunction to scroll to a specific pagescrollNext(instant?: boolean) => voidFunction to scroll to the next pagescrollPrev(instant?: boolean) => voidFunction to scroll to the previous pagegetProgress() => numberReturns the current scroll progress as a percentageplayVoidFunctionFunction to start/resume autoplaypauseVoidFunctionFunction to pause autoplayisInView(index: number) => booleanWhether the item is in viewrefreshVoidFunctionFunction to re-compute the snap points and clamp the page
Data Attributes
Edit this page on GitHub