Carousel
A 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 slide alignment (
start,center,end) - Supports showing multiple slides at a time
- Supports looping and auto-playing
- Supports custom spacing between slides
Installation
Install the carousel package:
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
Check the carousel anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the carousel package:
import * as carousel from "@zag-js/carousel"
The carousel package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
Then use the framework integration helpers:
Note: The carousel requires that you provide a
slideCountproperty in the machine context. This is the total number of slides.
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 orientation
Set orientation to vertical to render a vertical carousel.
const service = useMachine(carousel.machine, { orientation: "vertical", })
Setting the initial slide
Set defaultPage to define the initial page.
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, })
Controlling the current page
Use page and onPageChange for controlled navigation.
const service = useMachine(carousel.machine, { slideCount: 8, page, onPageChange(details) { setPage(details.page) }, })
Setting slides per page
Set slidesPerPage to control how many slides are visible per page.
const service = useMachine(carousel.machine, { slidesPerPage: 2, })
Setting slides per move
Set slidesPerMove to control how many slides advance on next/previous.
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.
Looping pages
Set loop to true to wrap around from last to first page.
const service = useMachine(carousel.machine, { loop: true, })
Setting the gap between slides
Set spacing to control the gap between slides.
const service = useMachine(carousel.machine, { spacing: "16px", })
Setting viewport padding
Set padding to keep neighboring slides partially visible.
const service = useMachine(carousel.machine, { padding: "16px", })
Variable-width slides
Set autoSize to true when slides have different widths.
const service = useMachine(carousel.machine, { autoSize: true, })
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) }, })
Listening for drag and autoplay status
Use status callbacks to react to dragging and autoplay lifecycle changes.
const service = useMachine(carousel.machine, { onDragStatusChange(details) { console.log(details.type, details.isDragging) }, onAutoplayStatusChange(details) { console.log(details.type, details.isPlaying) }, })
Dragging the carousel
Set allowMouseDrag to true to drag the carousel with a mouse.
const service = useMachine(carousel.machine, { allowMouseDrag: true, })
Autoplaying the carousel
Set autoplay to true to start automatic slide movement.
const service = useMachine(carousel.machine, { autoplay: true, })
Alternatively, you can configure the autoplay interval by setting the delay
property.
const service = useMachine(carousel.machine, { autoplay: { delay: 2000 }, })
Customizing accessibility messages
Use translations to customize localized trigger, item, and progress text.
const service = useMachine(carousel.machine, { slideCount: 5, translations: { nextTrigger: "Next slide", prevTrigger: "Previous slide", indicator: (index) => `Go to slide ${index + 1}`, item: (index, count) => `Slide ${index + 1} of ${count}`, autoplayStart: "Start autoplay", autoplayStop: "Stop autoplay", progressText: ({ page, totalPages }) => `Page ${page + 1} of ${totalPages}`, }, })
Styling guide
Each part includes a data-part attribute you can target in CSS.
[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.autoSizebooleanWhether to enable variable width slides.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 percentagegetProgressText() => stringReturns the progress textplayVoidFunctionFunction 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