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>

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>

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 select event fires — when the active scroll snap changes.
  • The reinit event 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>

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 loop enabled, goTo will automatically choose the shortest path to the target snap, scrolling forward or backward as needed.
  • No-op behavior: Calling goTo when the carousel is already at the given snap has no effect. This is expected and ensures navigation remains predictable and consistent.
Edit this page on GitHub