Slide Gaps
This guide shows you how to add spacing between slides in Embla Carousel.
All examples use the padding approach to create gaps between slides. Although you can also use other CSS techniques like margin or gap on the container, this guide will explain why padding is often easier to work with, with minimal hassle compared to its alternatives.
Prerequisites
Before continuing with this guide, make sure you've completed the following:
Understanding gaps
The recommended way to create spacing between slides is to use padding on the slides themselves combined with a corresponding negative margin on the container.
To make padding predictable and included in the element's width (so it doesn't add extra size), you should apply box-sizing: border-box globally, which is standard practice on most sites (and often included in reset.css or similar):
*,*::before,*::after { box-sizing: border-box;}Setting gaps
With box-sizing: border-box in place, you can adjust the --slide-spacing: variable in your CSS to change slide gap sizes:
.embla { --slide-size: 70%; --slide-spacing: 10px;}
.embla__viewport { overflow: hidden;}
.embla__container { display: flex; touch-action: pan-y pinch-zoom; margin-left: calc(var(--slide-spacing) * -1); /* Renders -10px */}
.embla__slide { flex: 0 0 var(--slide-size); min-width: 0; padding-left: var(--slide-spacing);}The benefits of the padding approach include:
- Keeps spacing consistent: If slide sizes change, the gaps remain correct and the carousel continues to work as expected.
- Automatic updates: Embla uses
ResizeObserverto track slide sizes, so changes to gap sizes are picked up automatically because the padding is included in the slide size. - Supports variable-width slides: Each slide can have a different width, and the spacing still works without additional configuration.
- RTL-friendly: The approach works even if the carousel is flipped to right-to-left, without any modifications.
- Compatible with Embla SSR feature: This method works when Embla is configured with server-side rendering, helping to avoid layout shifts for looped carousels and other scenarios.
Breakpoints
To make slide gaps responsive with the padding approach, use CSS media queries.
Because Embla relies on a ResizeObserver to track each slide's computed dimensions, it automatically recalculates scroll snaps whenever a slide's size changes. Since the padding is included in each slide's computed size, any gap adjustments made via media queries are detected automatically — no extra code required.
.embla { --slide-size: 70%; --slide-spacing: 10px; --slide-spacing-md-up: 20px;}
.embla__container { display: flex; touch-action: pan-y pinch-zoom; margin-left: calc(var(--slide-spacing) * -1); /* Renders -10px */}
.embla__slide { flex: 0 0 var(--slide-size); min-width: 0; padding-left: var(--slide-spacing);}
@media (min-width: 768px) { .embla__container { margin-left: calc(var(--slide-spacing-md-up) * -1); /* Renders -20px */ }
.embla__slide { padding-left: var(--slide-spacing-md-up); }}In view configuration
When using the padding approach for slide gaps, that padding becomes part of each slide's bounding rectangle. As a result, the slidesinview event will report a slide as “in view” even when only its padding — not the actual content — is visible.
To fix this, shrink the effective observation area by applying a negative root margin that matches your horizontal padding. For example, if each slide has 20px padding-left, you would set inViewMargin: '0px -20px 0px 0px' for a horizontal ltr carousel:
import EmblaCarousel from 'embla-carousel'
const wrapperNode = document.querySelector('.embla')const viewportNode = wrapperNode.querySelector('.embla__viewport')
const emblaApi = EmblaCarousel(viewportNode, { inViewMargin: '0px -20px 0px 0px'})
const logSlidesInView = (emblaApi, event) => { console.log('Slides entered view: ' + event.detail.slidesEnterView)}
emblaApi.on('slidesinview', logSlidesInView)<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>import React, { useEffect } from 'react'import useEmblaCarousel from 'embla-carousel-react'
export function EmblaCarousel() { const [emblaRef, emblaApi] = useEmblaCarousel({ inViewMargin: '0px -20px 0px 0px' })
const logSlidesInView = (emblaApi, event) => { console.log('Slides entered view: ' + event.detail.slidesEnterView) }
useEffect(() => { if (!emblaApi) return emblaApi.on('slidesinview', logSlidesInView) }, [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> )}<script setup>import { watch } from 'vue'import useEmblaCarousel from 'embla-carousel-vue'
const [emblaRef, emblaApi] = useEmblaCarousel({ inViewMargin: '0px -20px 0px 0px'})
const logSlidesInView = (emblaApi, event) => { console.log('Slides entered view: ' + event.detail.slidesEnterView)}
watch( emblaApi, (api) => { if (!api) return api.on('slidesinview', logSlidesInView) }, { 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></template>import { createEffect, on } from 'solid-js'import useEmblaCarousel from 'embla-carousel-solid'
export function EmblaCarousel() { const [emblaRef, emblaApi] = useEmblaCarousel(() => ({ inViewMargin: '0px -20px 0px 0px' }))
const logSlidesInView = (emblaApi, event) => { console.log('Slides entered view: ' + event.detail.slidesEnterView) }
createEffect( on(emblaApi, (api) => { if (!api) return api.on('slidesinview', logSlidesInView) }) )
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> )}<script> import useEmblaCarousel from 'embla-carousel-svelte'
let emblaApi let options = { inViewMargin: '0px -20px 0px 0px' }
const logSlidesInView = (emblaApi, event) => { console.log('Slides entered view: ' + event.detail.slidesEnterView) }
const onInit = (event) => { emblaApi = event.detail emblaApi.on('slidesinview', logSlidesInView) }</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>Other methods
If you still prefer not to use the padding approach, there are a few things to be aware of.
Embla will work with other CSS techniques such as gap or margin, but the trade-offs stem from how CSS spacing properties behave, not from any limitation in Embla itself.
Gap or margin
Warning: When using margin or gap, it's important to get your CSS
calculations right, because these properties add to the total slide size.
For example, to achieve two slides per view with a gap or margin of 20px between each slide, you need to account for the extra space added by these properties:
- Total gap per view is
20px, because we have two slides (one gap between them). - If you set
gap: 20pxand each slide to50%, the total width becomes50% + 50% + 20px, which exceeds the viewport width of100%. - Each slide needs to be
50%of the viewport width minus half the total gap per view — in this case,10px— to align correctly.
Try adjusting the slide size and gap below to display multiple slides per view — for example, two or three per view. The --slide-size variable is wrapped in a calc(), allowing you to write expressions directly.
Embla reads the computed slide dimensions, so scroll snaps are based on these expanded sizes. To achieve an exact fit for two slides per view, you need to adjust the flex-basis and use calc():
.embla__container { gap: 20px;}
.embla__slide { flex: 0 0 calc(50% - 10px); min-width: 0;}Warning: Unlike padding, changes in margin or gap do not affect a slide's computed size, so Embla's ResizeObserver won't detect them automatically. If you update the gap or margin dynamically (e.g., via media queries or JavaScript), you must manually call:
emblaApi.reInit()Gap with loop enabled
When using gap with loop: true, there will be no gap between the last and first slide. This is because CSS gap only applies between items, and Embla uses the computed spacing between slides to calculate distances.
A simple workaround is to add a margin to the last slide that matches the gap size.
.embla__slide:last-child { margin-right: 20px;}Caveats
- Margins and gaps add to slide width, which can easily break layouts if not accounted for.
- Requires manual math adjustments (often with
calc()) for precise alignment. - More error-prone for responsive or variable-width carousels unless all slide sizes and spacing are carefully calculated.
- Gap will not apply between the first and last slide in loop: true unless additional margin is added.
While both gap and margin can work, padding on slides combined with a negative margin on the container remains the most robust, predictable, and maintenance-friendly approach, which supports looped, responsive, and SSR-enabled carousels without the need for additional modifications.