Server-Side Rendering

This guide explains how to configure Embla Carousel for server-side rendering (SSR) environments and when to use SSR-specific settings.


Prerequisites

Before continuing with this guide, make sure you've completed the following:

Understanding SSR

Embla Carousel is environment-agnostic, meaning it works in both client-side and server-side contexts. In many cases, you can render the carousel markup on the server without any SSR-specific configuration, and everything will work as expected.

However, there are scenarios where you may want to prevent layout shifts caused by Embla's initialization when the browser dimensions become available. In such cases, Embla provides optional SSR tools so the carousel renders with stable positioning immediately.

You should enable SSR support if any of these are true:

Setting Up SSR

Start by ensuring your carousel's CSS uses percentage-based slide widths. Here's an example where each slide takes up 50% of the viewport width:

.embla__viewport {  overflow: hidden;}
.embla__container {  display: flex;  touch-action: pan-y pinch-zoom;}
.embla__slide {  flex: 0 0 50%; /* Each slide is 50% of the viewport width */  min-width: 0;}

Next, enable SSR support by configuring the ssr option and applying server-generated styles using the ssrStyles method.

You'll specify slide widths as percentages (%), exactly matching your CSS:

// SERVERimport EmblaCarousel from 'embla-carousel'
export function getEmblaCarouselSsr(slides, carouselId) {  const emblaServerApi = EmblaCarousel(null, {    loop: true,    ssr: slides.map(() => 50) // Each slide is 50% of the viewport width  })
  return `    <style>      ${emblaServerApi.ssrStyles(`#${carouselId}`, '.embla__slide')}    </style>
    <div class="embla">      <div class="embla__viewport">        <div class="embla__container" id="${carouselId}">          ${slides            .map((slide) => `<div class="embla__slide">${slide}</div>`)            .join('')}        </div>      </div>    </div>  `}
// CLIENTimport EmblaCarousel from 'embla-carousel'
const wrapperNode = document.querySelector('.embla')const viewportNode = wrapperNode.querySelector('.embla__viewport')const emblaApi = EmblaCarousel(viewportNode, {  loop: true,  ssr: slides.map(() => 50) // Each slide is 50% of the viewport width})

Warning: Your CSS widths (such as flex: 0 0 50%) must exactly match the values in the ssr array. Any mismatch will result in incorrect snapping and layout shifts during hydration.

Since the server has no access to runtime measurements, you must explicitly define the slide sizes. This introduces a few key requirements:

  • Slides must use percentage-based widths.
  • Slide spacing must use the padding method (margin and gap are not supported during SSR).
  • CSS widths must exactly match the values in ssr.
  • Each carousel must have a unique selector to avoid SSR styles affecting others on the page.

Note: SSR also fully supports vertical carousels, as long as the CSS is configured so that slides stack vertically (e.g. flex-direction: column) and the percentage sizes apply to height instead of width. The same SSR rules apply — the CSS values must match the values provided in the ssr array.

Breakpoints

Embla also supports SSR configurations that change with CSS breakpoints. Start by defining your CSS with different slide widths per viewport size:

.embla__viewport {  overflow: hidden;}
.embla__container {  display: flex;  touch-action: pan-y pinch-zoom;}
.embla__slide {  flex: 0 0 50%; /* Each slide is 50% of the viewport width */  min-width: 0;}
@media (min-width: 768px) {  .embla__slide {    flex: 0 0 70%; /* Each slide is 70% of the viewport width */  }}

Then provide matching ssr values for each breakpoint using the breakpoints option. You can define unique slide sizes and settings for each breakpoint, and Embla will apply them correctly on the server.

// SERVERimport EmblaCarousel from 'embla-carousel'
export function getEmblaCarouselSsr(slides, carouselId) {  const emblaServerApi = EmblaCarousel(null, {    loop: true,    ssr: slides.map(() => 50), // Each slide is 50% of the viewport width    breakpoints: {      '(min-width: 768px)': {        ssr: slides.map(() => 70) // Each slide is 70% of the viewport width      }    }  })
  return `    <style>      ${emblaServerApi.ssrStyles(`#${carouselId}`, '.embla__slide')}    </style>
    <div class="embla">      <div class="embla__viewport">        <div class="embla__container" id="${carouselId}">          ${slides            .map((slide) => `<div class="embla__slide">${slide}</div>`)            .join('')}        </div>      </div>    </div>  `}
// CLIENTimport EmblaCarousel from 'embla-carousel'
const wrapperNode = document.querySelector('.embla')const viewportNode = wrapperNode.querySelector('.embla__viewport')const emblaApi = EmblaCarousel(viewportNode, {  loop: true,  ssr: slides.map(() => 50), // Each slide is 50% of the viewport width  breakpoints: {    '(min-width: 768px)': {      ssr: slides.map(() => 70) // Each slide is 70% of the viewport width    }  }})
Edit this page on GitHub