Embla Carousel with Arrows, Dots and Keyboard Navigation — Free Snippet

Embla Carousel with Arrows, Dots and Keyboard Navigation · Carousels · Plain HTML, CSS & JS · Live preview

What's included

Features

Swipe and drag
Momentum and snapping from Embla's engine.
Snap-based dots
Built from scrollSnapList, not the slide count.
Edge-aware arrows
Disabled with canScrollPrev and canScrollNext.
Keyboard navigation
Arrow keys on a focusable viewport.
ARIA carousel pattern
Roledescriptions and per-slide labels.
Runtime reInit
Loop toggled without recreating the carousel.
Peeking layout
82% slides show the next slide's edge.
Mobile-safe touch
touch-action keeps vertical scrolling working.

About this UI Snippet

Embla Carousel Basics — Arrows, Dots, Keyboard and Loop Done Properly

Screenshot of the Embla Carousel with Arrows, Dots and Keyboard Navigation snippet rendered live

Embla Carousel is a small, dependency-free carousel engine. It handles dragging, momentum, snapping and looping, and deliberately renders no UI of its own: arrows, dots and captions are your markup. That makes it a good library to learn from, because every control is a few lines of code you can read.

The three-element structure

Embla needs a viewport (overflow: hidden), a container inside it (display: flex), and slides inside that. Slide width is plain CSS — flex: 0 0 82% shows a peek of the next slide. Gaps use padding-left on slides and a negative margin on the container, which keeps snap positions exact.

Dots come from scroll snaps

A common bug is creating one dot per slide. Embla navigates between scroll snaps, and when containScroll trims the ends, several slides can share a snap. scrollSnapList() returns the real positions, so the dots always match what scrollTo(i) can reach.

Buttons that know the edges

On every select event, canScrollPrev() and canScrollNext() decide whether the arrow buttons are disabled. With looping on, both are always true.

Keyboard support is yours to add

Embla doesn't bind keys. The viewport is focusable (tabindex="0") and listens for the left and right arrow keys. The dots use aria-selected and a roving tabIndex, and the section uses aria-roledescription="carousel" with per-slide labels, following the WAI-ARIA carousel pattern.

Changing options at runtime

The loop checkbox calls embla.reInit({ loop }). The reInit event rebuilds the dots and button states, because snaps can change when options do.

Touch behaviour

touch-action: pan-y pinch-zoom on the container lets vertical page scrolling and pinch-zoom keep working on phones while horizontal swipes drive the carousel.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet into an AI assistant like Claude and ask it to explain the viewport-container-slide structure and why dots are generated from scroll snaps. Ask it to add a slide counter announced to screen readers, autoplay that pauses on focus, or responsive options that show two slides on wide screens using Embla's breakpoints option. It can also review the ARIA roles against the WAI-ARIA Authoring Practices carousel pattern.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an image carousel with Embla Carousel v8 (loaded from a CDN as a UMD script) in plain HTML, CSS and JavaScript.

Requirements:
- Use Embla's viewport, container and slide structure, with slides sized to 82% of the viewport so the next slide peeks in, and gaps created with slide padding and a negative container margin.
- Add previous and next buttons that are disabled when the carousel can't scroll further.
- Build pill-shaped dot buttons from the carousel's scroll snaps, highlight the selected one and let clicking scroll to that snap.
- Make the viewport focusable and support left and right arrow keys.
- Follow the WAI-ARIA carousel pattern with aria-roledescription on the carousel and slides, and per-slide labels like "1 of 5".
- Add a loop checkbox that re-initialises the carousel with the new option and rebuilds dots and button states.
- Keep vertical page scrolling working on touch devices.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<section class="emb" aria-roledescription="carousel" aria-label="Featured destinations">
  <div class="emb-viewport" tabindex="0">
    <div class="emb-container">
      <div class="emb-slide" role="group" aria-roledescription="slide" aria-label="1 of 5"><img src="https://picsum.photos/id/1036/900/560" alt="Snowy mountain ridge at dusk"><div class="emb-cap"><b>Alpine Ridge</b><span>Switzerland</span></div></div>
      <div class="emb-slide" role="group" aria-roledescription="slide" aria-label="2 of 5"><img src="https://picsum.photos/id/1043/900/560" alt="Coastal cliffs over the sea"><div class="emb-cap"><b>Sea Cliffs</b><span>Portugal</span></div></div>
      <div class="emb-slide" role="group" aria-roledescription="slide" aria-label="3 of 5"><img src="https://picsum.photos/id/1018/900/560" alt="Green valley under clouds"><div class="emb-cap"><b>Cloud Valley</b><span>New Zealand</span></div></div>
      <div class="emb-slide" role="group" aria-roledescription="slide" aria-label="4 of 5"><img src="https://picsum.photos/id/1039/900/560" alt="Waterfall in a forest"><div class="emb-cap"><b>Hidden Falls</b><span>Iceland</span></div></div>
      <div class="emb-slide" role="group" aria-roledescription="slide" aria-label="5 of 5"><img src="https://picsum.photos/id/1015/900/560" alt="River winding through a canyon"><div class="emb-cap"><b>River Canyon</b><span>Norway</span></div></div>
    </div>
  </div>
  <div class="emb-controls">
    <button class="emb-arrow" type="button" id="embPrev" aria-label="Previous slide">‹</button>
    <div class="emb-dots" id="embDots" role="tablist" aria-label="Choose slide"></div>
    <button class="emb-arrow" type="button" id="embNext" aria-label="Next slide">›</button>
  </div>
  <label class="emb-loop"><input type="checkbox" id="embLoop"> Loop</label>
</section>

Step by step

How to Use

  1. 1
    Load EmblaInclude embla-carousel.umd.js from the CDN; it exposes window.EmblaCarousel.
  2. 2
    Keep the structureViewport, then container, then slides.
  3. 3
    Size slides in CSSChange flex-basis on .emb-slide to show more or fewer slides.
  4. 4
    NavigateDrag, use the arrows, click dots, or focus the viewport and press arrow keys.
  5. 5
    Toggle loopThe checkbox calls reInit with new options.

Real-world uses

Common Use Cases

Travel and real-estate galleries
Swipeable photo cards with captions.
Product highlights
Feature cards on landing pages.
Content rows
Articles or videos in a horizontal strip.
Onboarding screens
Step through slides with dots.
Learning Embla
The minimal pattern every Embla build starts from.
Related: Embla Thumbnail Gallery
Add a synced thumbnail strip: Embla Carousel Thumbnail Gallery.
Related: Native Scroll-Snap Carousel
Compare with a no-library approach: Native Scroll-Snap Carousel.

Got questions?

Frequently Asked Questions

Load embla-carousel.umd.js from a CDN. It defines window.EmblaCarousel. Call EmblaCarousel(viewportElement, options) where the viewport has overflow hidden and contains a flex container of slides.

Embla moves between scroll snaps. With the default containScroll setting, slides near the ends can share a snap, so the number of snaps can be lower than the number of slides. Building dots from scrollSnapList keeps every dot reachable.

Listen to the select event and set each button's disabled property from canScrollPrev() and canScrollNext(). With loop enabled both return true.

Not by default. Make the viewport focusable and handle ArrowLeft and ArrowRight with scrollPrev and scrollNext, and make dots real buttons so they are reachable with Tab.

Call embla.reInit(newOptions). It keeps the instance and emits a reInit event, where you should rebuild anything derived from snaps, such as dots.