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
This command will install the framework agnostic carousel logic and the reactive utilities for your framework of choice.
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-part
attribute 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
id
to theuseMachine
hook. 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 🔥
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 [state, send] = useMachine( carousel.machine({ id: "1", slideCount: items.length }), ) const api = carousel.connect(state, send, 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 [state, send] = useMachine( carousel.machine({ id: createUniqueId(), slideCount: items.length }), ) const api = createMemo(() => carousel.connect(state, send, 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 [state, send] = useMachine( carousel.machine({ id: "1", slideCount: items.length }), ) const api = computed(() => carousel.connect(state.value, send, 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 [snapshot, send] = useMachine( carousel.machine({ id: "1", slideCount: items.length }), ) const api = $derived(carousel.connect(snapshot, send, 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 [state, send] = useMachine( carousel.machine({ orientation: "vertical", }), )
Setting the initial slide
To set the initial slide of the carousel, pass the page
property to the
machine's context.
The page
corresponds 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 [state, send] = useMachine( carousel.machine({ page: 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 [state, send] = 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
.
If the value is
auto
, the carousel will move the number of slides equal to the number of slides per page.
const [state, send] = useMachine( carousel.machine({ slidesPerMove: 2, }), )
Ensure the
slidesPerMove
is less than or equal to theslidesPerPage
to avoid skipping slides.
Setting the carousel should loop around
To allow looping of slides, set the loop
property in the machine's context to
true
.
const [state, send] = 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 [state, send] = useMachine( carousel.machine({ spacing: "16px", }), )
Listening for page changes
When the carousel page changes, the onPageChange
callback is invoked.
const [state, send] = 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 [state, send] = 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 [state, send] = useMachine( carousel.machine({ autoplay: true, }), )
Alternatively, you can configure the autoplay interval by setting the delay
property in the machine's context.
const [state, send] = 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:
ids
Partial<{ 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.translations
IntlTranslations
The localized messages to use.slidesPerPage
number
The number of slides to show at a time.slidesPerMove
number | "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.autoplay
boolean | { delay: number; }
Whether to scroll automatically. The default delay is 4000ms.allowMouseDrag
boolean
Whether to allow scrolling via dragging with mouseloop
boolean
Whether the carousel should loop around.page
number
The index of the active page.spacing
string
The amount of space between items.padding
string
Defines the extra space added around the scrollable area, enabling nearby items to remain partially in view.onPageChange
(details: PageChangeDetails) => void
Function called when the page changes.inViewThreshold
number | number[]
The threshold for determining if an item is in view.snapType
"proximity" | "mandatory"
The snap type of the item.slideCount
number
The total number of slides. Useful for SSR to render the initial ating the snap points.onDragStatusChange
(details: DragStatusDetails) => void
Function called when the drag status changes.onAutoplayStatusChange
(details: AutoplayStatusDetails) => void
Function called when the autoplay status changes.dir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.orientation
Orientation
The orientation of the element.
Machine API
The carousel api
exposes the following methods:
page
number
The current index of the carouselpageSnapPoints
number[]
The current snap points of the carouselisPlaying
boolean
Whether the carousel is auto playingisDragging
boolean
Whether the carousel is being dragged. This only works when `draggable` is true.canScrollNext
boolean
Whether the carousel is can scroll to the next viewcanScrollPrev
boolean
Whether the carousel is can scroll to the previous viewscrollToIndex
(index: number, instant?: boolean) => void
Function to scroll to a specific item indexscrollTo
(page: number, instant?: boolean) => void
Function to scroll to a specific pagescrollNext
(instant?: boolean) => void
Function to scroll to the next pagescrollPrev
(instant?: boolean) => void
Function to scroll to the previous pagegetProgress
() => number
Returns the current scroll progress as a percentageplay
() => void
Function to start/resume autoplaypause
() => void
Function to pause autoplayisInView
(index: number) => boolean
Whether the item is in viewrefresh
() => void
Function to re-compute the snap points and clamp the page
Data Attributes
Edit this page on GitHub