Dot Buttons
This guide shows you how to dot buttons to control carousel navigation in Embla Carousel.
Prerequisites
Button placement
If your carousel is draggable, the root node — the element passed to the EmblaCarousel initializer (e.g., .embla__viewport) — responds to pointer events. To avoid unintended drag interactions when users click them, place your navigation buttons outside the root element, like this:
import EmblaCarousel from 'embla-carousel'
const wrapperNode = document.querySelector('.embla')const viewportNode = wrapperNode.querySelector('.embla__viewport')const emblaApi = EmblaCarousel(viewportNode, { loop: false })<div class="embla"> <div class="embla__viewport"> <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>
<!-- Place your navigation buttons here --></div>import React from 'react'import useEmblaCarousel from 'embla-carousel-react'
export function EmblaCarousel() { const [emblaRef] = useEmblaCarousel({ loop: false })
return ( <div className="embla"> <div className="embla__viewport" ref={emblaRef}> <div className="embla__container"> <div className="embla__slide">Slide 1</div> <div className="embla__slide">Slide 2</div> <div className="embla__slide">Slide 3</div> </div> </div>
{/* Place your navigation buttons here */} </div> )}<script setup>import useEmblaCarousel from 'embla-carousel-vue'
const [emblaRef] = useEmblaCarousel({ loop: false })</script>
<template> <div class="embla"> <div class="embla__viewport" ref="emblaRef"> <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>
<!-- Place your navigation buttons here --> </div></template>import useEmblaCarousel from 'embla-carousel-solid'
export function EmblaCarousel() { const [emblaRef] = useEmblaCarousel(() => ({ loop: false }))
return ( <div class="embla"> <div class="embla__viewport" ref={emblaRef}> <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>
{/* Place your navigation buttons here */} </div> )}<script> import useEmblaCarousel from 'embla-carousel-svelte'
let options = { loop: false }</script>
<div class="embla"> <div class="embla__viewport" use:useEmblaCarousel={{ 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>
<!-- Place your navigation buttons here --></div>Adding buttons
Embla Carousel exposes the total number of snap points — one for each group of slides — through the snapList method. You can use this to generate a dot button for each snap dynamically.
Each button should be connected to Embla's goTo method, which scrolls the carousel to a specific snap index. When users click a dot, the carousel will smoothly move to the corresponding slide group, providing an intuitive navigation experience.
import EmblaCarousel from 'embla-carousel'
const wrapperNode = document.querySelector('.embla')const viewportNode = wrapperNode.querySelector('.embla__viewport')const dotsNode = wrapperNode.querySelector('.embla__dots')const emblaApi = EmblaCarousel(viewportNode, { loop: false })
let dotNodes = []
const createDotButtonHtml = (emblaApi, dotsNode) => { const dotTemplate = document.getElementById('embla-dot-template') const snapList = emblaApi.snapList() dotsNode.innerHTML = snapList.reduce((acc) => acc + dotTemplate.innerHTML, '') return Array.from(dotsNode.querySelectorAll('.embla__dot'))}
const addDotButtonClickHandlers = (emblaApi, dotNodes) => { dotNodes.forEach((dotNode, index) => { dotNode.addEventListener('click', () => emblaApi.scrollToSnap(index), false) })}
const createAndSetupDotButtons = (emblaApi, dotsNode) => { dotNodes = createDotButtonHtml(emblaApi, dotsNode) addDotButtonClickHandlers(emblaApi, dotNodes)}
createAndSetupDotButtons(emblaApi, dotsNode)emblaApi.on('reinit', () => createAndSetupDotButtons(emblaApi, dotsNode))<div class="embla"> <div class="embla__viewport"> <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>
<div class="embla__dots"></div>
<script type="text/template" id="embla-dot-template"> <button class="embla__dot"> <!-- Button content --> </button> </script></div>import React, { useState, useEffect } from 'react'import useEmblaCarousel from 'embla-carousel-react'
export function EmblaCarousel() { const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false }) const [scrollSnaps, setScrollSnaps] = useState([])
const goTo = (index) => emblaApi?.goTo(index) const setupSnaps = (emblaApi) => setScrollSnaps(emblaApi.snapList())
useEffect(() => { if (!emblaApi) return
setupSnaps(emblaApi) emblaApi.on('reinit', setupSnaps) }, [emblaApi])
return ( <div className="embla"> <div className="embla__viewport" ref={emblaRef}> <div className="embla__container"> <div className="embla__slide">Slide 1</div> <div className="embla__slide">Slide 2</div> <div className="embla__slide">Slide 3</div> </div> </div>
<div className="embla__dots"> {scrollSnaps.map((_, index) => ( <button className="embla__dot" key={index} onClick={() => goTo(index)} > {/* Button content */} </button> ))} </div> </div> )}<script setup>import { ref, watch } from 'vue'import useEmblaCarousel from 'embla-carousel-vue'
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false })const scrollSnaps = ref([])
const goTo = (index) => emblaApi.value?.goTo(index)const setupSnaps = (emblaApi) => (scrollSnaps.value = emblaApi.snapList())
watch( emblaApi, (api) => { if (!api) return
setupSnaps(api) api.on('reinit', setupSnaps) }, { immediate: true })</script>
<template> <div class="embla"> <div class="embla__viewport" ref="emblaRef"> <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>
<div class="embla__dots"> <button class="embla__dot" :key="index" v-for="(_, index) in scrollSnaps" @click="goTo(index)" > <!-- Button content --> </button> </div> </div></template>import { createSignal, createEffect, on, For } from 'solid-js'import useEmblaCarousel from 'embla-carousel-solid'
export function EmblaCarousel() { const [emblaRef, emblaApi] = useEmblaCarousel(() => ({ loop: false })) const [scrollSnaps, setScrollSnaps] = createSignal([])
const goTo = (index) => emblaApi()?.goTo(index) const setupSnaps = (emblaApi) => setScrollSnaps(emblaApi.snapList())
createEffect( on(emblaApi, (api) => { if (!api) return
setupSnaps(api) api.on('reinit', setupSnaps) }) )
return ( <div class="embla"> <div class="embla__viewport" ref={emblaRef}> <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>
<div class="embla__dots"> <For each={scrollSnaps()}> {(_, index) => ( <button class="embla__dot" onClick={() => goTo(index())}> {/* Button content */} </button> )} </For> </div> </div> )}<script> import useEmblaCarousel from 'embla-carousel-svelte'
let emblaApi let options = { loop: false } let scrollSnaps = []
const goTo = (index) => emblaApi?.goTo(index) const setupSnaps = (emblaApi) => (scrollSnaps = emblaApi.snapList())
const onInit = (event) => { emblaApi = event.detail
setupSnaps(emblaApi) emblaApi.on('reinit', setupSnaps) }</script>
<div class="embla"> <div class="embla__viewport" on:emblainit={onInit} use:useEmblaCarousel={{ 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>
<div class="embla__dots"> {#each scrollSnaps as _, index} <button class="embla__dot" on:click={() => goTo(index)}> <!-- Button content --> </button> {/each} </div></div>Buttons state
To improve the user experience, let's add visual feedback to show which dot button corresponds to the currently selected snap.
You can achieve this by toggling each button's state between selected and not selected, and keeping them in sync whenever the carousel updates. Update the buttons whenever:
- The
selectevent fires — when the active scroll snap changes. - The
reinitevent fires — when the carousel reinitializes.
.embla__dot { opacity: 0.5;}
.embla__dot--selected { opacity: 1;}import EmblaCarousel from 'embla-carousel'
const wrapperNode = document.querySelector('.embla')const viewportNode = wrapperNode.querySelector('.embla__viewport')const dotsNode = wrapperNode.querySelector('.embla__dots')const emblaApi = EmblaCarousel(viewportNode, { loop: false })
let dotNodes = []
const createDotButtonHtml = (emblaApi, dotsNode) => { const dotTemplate = document.getElementById('embla-dot-template') const snapList = emblaApi.snapList() dotsNode.innerHTML = snapList.reduce((acc) => acc + dotTemplate.innerHTML, '') return Array.from(dotsNode.querySelectorAll('.embla__dot'))}
const addDotButtonClickHandlers = (emblaApi, dotNodes) => { dotNodes.forEach((dotNode, index) => { dotNode.addEventListener('click', () => emblaApi.scrollToSnap(index), false) })}
const toggleDotButtonsActive = (emblaApi, dotNodes) => { if (!dotNodes.length) return const previous = emblaApi.previousSnap() const selected = emblaApi.selectedSnap() dotNodes[previous].classList.remove('embla__dot--selected') dotNodes[selected].classList.add('embla__dot--selected')}
const createAndSetupDotButtons = (emblaApi, dotsNode) => { dotNodes = createDotButtonHtml(emblaApi, dotsNode) addDotButtonClickHandlers(emblaApi, dotNodes) toggleDotButtonsActive(emblaApi, dotNodes)}
createAndSetupDotButtons(emblaApi, dotsNode)emblaApi.on('reinit', () => createAndSetupDotButtons(emblaApi, dotsNode))emblaApi.on('select', (emblaApi) => toggleDotButtonsActive(emblaApi, dotNodes))<div class="embla"> <div class="embla__viewport"> <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>
<div class="embla__dots"></div>
<script type="text/template" id="embla-dot-template"> <button class="embla__dot"> <!-- Button content --> </button> </script></div>import React, { useState, useEffect } from 'react'import useEmblaCarousel from 'embla-carousel-react'
export function EmblaCarousel() { const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false }) const [scrollSnaps, setScrollSnaps] = useState([]) const [selectedSnap, setSelectedSnap] = useState(0)
const goTo = (index) => emblaApi?.goTo(index) const setupSnaps = (emblaApi) => setScrollSnaps(emblaApi.snapList()) const setActiveSnap = (emblaApi) => setSelectedSnap(emblaApi.selectedSnap())
useEffect(() => { if (!emblaApi) return
setupSnaps(emblaApi) setActiveSnap(emblaApi)
emblaApi.on('reinit', setupSnaps) emblaApi.on('reinit', setActiveSnap) emblaApi.on('select', setActiveSnap) }, [emblaApi])
return ( <div className="embla"> <div className="embla__viewport" ref={emblaRef}> <div className="embla__container"> <div className="embla__slide">Slide 1</div> <div className="embla__slide">Slide 2</div> <div className="embla__slide">Slide 3</div> </div> </div>
<div className="embla__dots"> {scrollSnaps.map((_, index) => ( <button className={'embla__dot'.concat( index === selectedSnap ? ' embla__dot--selected' : '' )} key={index} onClick={() => goTo(index)} > {/* Button content */} </button> ))} </div> </div> )}<script setup>import { ref, watch } from 'vue'import useEmblaCarousel from 'embla-carousel-vue'
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false })const scrollSnaps = ref([])const selectedSnap = ref(0)
const goTo = (index) => emblaApi.value?.goTo(index)const setupSnaps = (emblaApi) => (scrollSnaps.value = emblaApi.snapList())const setActiveSnap = (emblaApi) => (selectedSnap.value = emblaApi.selectedSnap())
watch( emblaApi, (api) => { if (!api) return
setupSnaps(api) setActiveSnap(api)
api.on('reinit', setupSnaps) api.on('reinit', setActiveSnap) api.on('select', setActiveSnap) }, { immediate: true })</script>
<template> <div class="embla"> <div class="embla__viewport" ref="emblaRef"> <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>
<div class="embla__dots"> <button :class="[ 'embla__dot', index === selectedSnap ? 'embla__dot--selected' : '' ]" :key="index" v-for="(_, index) in scrollSnaps" @click="goTo(index)" > <!-- Button content --> </button> </div> </div></template>import { createSignal, createEffect, on, For } from 'solid-js'import useEmblaCarousel from 'embla-carousel-solid'
export function EmblaCarousel() { const [emblaRef, emblaApi] = useEmblaCarousel(() => ({ loop: false })) const [scrollSnaps, setScrollSnaps] = createSignal([]) const [selectedSnap, setSelectedSnap] = createSignal(0)
const goTo = (index) => emblaApi()?.goTo(index) const setupSnaps = (emblaApi) => setScrollSnaps(emblaApi.snapList()) const setActiveSnap = (emblaApi) => setSelectedSnap(emblaApi.selectedSnap())
createEffect( on(emblaApi, (api) => { if (!api) return
setupSnaps(api) setActiveSnap(api)
api.on('reinit', setupSnaps) api.on('reinit', setActiveSnap) api.on('select', setActiveSnap) }) )
return ( <div class="embla"> <div class="embla__viewport" ref={emblaRef}> <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>
<div class="embla__dots"> <For each={scrollSnaps()}> {(_, index) => ( <button class={'embla__dot'.concat( index() === selectedSnap() ? ' embla__dot--selected' : '' )} onClick={() => goTo(index())} > {/* Button content */} </button> )} </For> </div> </div> )}<script> import useEmblaCarousel from 'embla-carousel-svelte'
let emblaApi let options = { loop: false } let scrollSnaps = [] let selectedSnap = 0
const goTo = (index) => emblaApi?.goTo(index) const setupSnaps = (emblaApi) => (scrollSnaps = emblaApi.snapList()) const setActiveSnap = (emblaApi) => (selectedSnap = emblaApi.selectedSnap())
const onInit = (event) => { emblaApi = event.detail
setupSnaps(emblaApi) setActiveSnap(emblaApi)
emblaApi.on('reinit', setupSnaps) emblaApi.on('reinit', setActiveSnap) emblaApi.on('select', setActiveSnap) }</script>
<div class="embla"> <div class="embla__viewport" on:emblainit={onInit} use:useEmblaCarousel={{ 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>
<div class="embla__dots"> {#each scrollSnaps as _, index} <button class="embla__dot" class:embla__dot--selected={index === selectedSnap} on:click={() => goTo(index)} > <!-- Button content --> </button> {/each} </div></div>Notes
- Button placement: Place the previous/next buttons outside the Embla root element (the one passed to the initializer, e.g.,
.embla__viewport) to avoid accidental drag interactions when users click them. - Loop behavior: When the carousel has
loopenabled,goTowill automatically choose the shortest path to the target snap, scrolling forward or backward as needed. - No-op behavior: Calling
goTowhen the carousel is already at the given snap has no effect. This is expected and ensures navigation remains predictable and consistent.