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:
loopistrue.containScrollisfalse.startSnapis set to anything other than0.
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})import React, { useId } from 'react'import useEmblaCarousel from 'embla-carousel-react'
export function EmblaCarousel(props) { const carouselId = useId() const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel({ loop: true, ssr: props.slides.map(() => 50) // Each slide is 50% of the viewport width }) const renderSsrStyles = !emblaApi
return ( <> {renderSsrStyles && ( <style> {emblaServerApi.ssrStyles(`#${carouselId}`, '.embla__slide')} </style> )}
<div className="embla"> <div className="embla__viewport" ref={emblaRef}> <div className="embla__container" id={carouselId}> {props.slides.map((slide) => ( <div className="embla__slide" key={slide.id}> {slide} </div> ))} </div> </div> </div> </> )}<script setup>import { computed } from 'vue'import useEmblaCarousel from 'embla-carousel-vue'
const props = defineProps({ carouselId: String, slides: { type: Array, required: true }})
const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel({ ssr: props.slides.map(() => 50) // Each slide is 50% of the viewport width})const renderSsrStyles = computed(() => !emblaApi.value)
const ssrStyles = ` <style> ${emblaServerApi.ssrStyles(`#${props.carouselId}`, '.embla__slide')} </style>`</script>
<template> <div v-if="renderSsrStyles" v-html="ssrStyles"></div>
<div class="embla"> <div class="embla__viewport" ref="emblaRef"> <div class="embla__container" :id="props.carouselId"> <div class="embla__slide" v-for="(slide, index) in props.slides" :key="index" > {{ slide }} </div> </div> </div> </div></template>import { For } from 'solid-js'import useEmblaCarousel from 'embla-carousel-solid'
export function EmblaCarousel(props) { const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel(() => ({ loop: true, ssr: props.slides.map(() => 50) // Each slide is 50% of the viewport width }))
const renderSsrStyles = () => !emblaApi()
return ( <> {renderSsrStyles() && ( <style> {emblaServerApi.ssrStyles(`#${props.carouselId}`, '.embla__slide')} </style> )}
<div class="embla"> <div class="embla__viewport" ref={emblaRef}> <div class="embla__container" id={props.carouselId}> <For each={props.slides}> {(slide) => <div class="embla__slide">{slide}</div>} </For> </div> </div> </div> </> )}<script> import useEmblaCarousel from 'embla-carousel-svelte'
export let slides export let carouselId
const options = { loop: true, ssr: slides.map(() => 50) // Each slide is 50% of the viewport width }
let emblaApi const emblaServerApi = useEmblaCarousel({ options })
$: renderSsrStyles = !emblaApi const ssrStyles = ` <style> ${emblaServerApi.ssrStyles(`#${carouselId}`, '.embla__slide')} </style> `
const onInit = (event) => { emblaApi = event.detail }</script>
{#if renderSsrStyles} <div> {@html ssrStyles} </div>{/if}
<div class="embla"> <div class="embla__viewport" use:useEmblaCarousel={{ options }} on:emblainit={onInit} > <div class="embla__container" id={carouselId}> {#each slides as slide} <div class="embla__slide">{slide}</div> {/each} </div> </div></div>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 (
marginandgapare 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 } }})import React, { useId } from 'react'import useEmblaCarousel from 'embla-carousel-react'
export function EmblaCarousel(props) { const carouselId = useId() const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel({ loop: true, ssr: props.slides.map(() => 50), // Each slide is 50% of the viewport width breakpoints: { '(min-width: 768px)': { ssr: props.slides.map(() => 70) // Each slide is 70% of the viewport width } } }) const renderSsrStyles = !emblaApi
return ( <> {renderSsrStyles && ( <style> {emblaServerApi.ssrStyles(`#${carouselId}`, '.embla__slide')} </style> )}
<div className="embla"> <div className="embla__viewport" ref={emblaRef}> <div className="embla__container" id={carouselId}> {props.slides.map((slide) => ( <div className="embla__slide" key={slide.id}> {slide} </div> ))} </div> </div> </div> </> )}<script setup>import { computed } from 'vue'import useEmblaCarousel from 'embla-carousel-vue'
const props = defineProps({ carouselId: String, slides: { type: Array, required: true }})
const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel({ ssr: props.slides.map(() => 50), // Each slide is 50% of the viewport width breakpoints: { '(min-width: 768px)': { ssr: props.slides.map(() => 70) // Each slide is 70% of the viewport width } }})const renderSsrStyles = computed(() => !emblaApi.value)
const ssrStyles = ` <style> ${emblaServerApi.ssrStyles(`#${props.carouselId}`, '.embla__slide')} </style>`</script>
<template> <div v-if="renderSsrStyles" v-html="ssrStyles"></div>
<div class="embla"> <div class="embla__viewport" ref="emblaRef"> <div class="embla__container" :id="props.carouselId"> <div class="embla__slide" v-for="(slide, index) in props.slides" :key="index" > {{ slide }} </div> </div> </div> </div></template>import { For } from 'solid-js'import useEmblaCarousel from 'embla-carousel-solid'
export function EmblaCarousel(props) { const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel(() => ({ loop: true, ssr: props.slides.map(() => 50), // Each slide is 50% of the viewport width breakpoints: { '(min-width: 768px)': { ssr: props.slides.map(() => 70) // Each slide is 70% of the viewport width } } }))
const renderSsrStyles = () => !emblaApi()
return ( <> {renderSsrStyles() && ( <style> {emblaServerApi.ssrStyles(`#${props.carouselId}`, '.embla__slide')} </style> )}
<div class="embla"> <div class="embla__viewport" ref={emblaRef}> <div class="embla__container" id={props.carouselId}> <For each={props.slides}> {(slide) => <div class="embla__slide">{slide}</div>} </For> </div> </div> </div> </> )}<script> import useEmblaCarousel from 'embla-carousel-svelte'
export let slides export let carouselId
const options = { 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 } } }
let emblaApi const emblaServerApi = useEmblaCarousel({ options })
$: renderSsrStyles = !emblaApi const ssrStyles = ` <style> ${emblaServerApi.ssrStyles(`#${carouselId}`, '.embla__slide')} </style> `
const onInit = (event) => { emblaApi = event.detail }</script>
{#if renderSsrStyles} <div> {@html ssrStyles} </div>{/if}
<div class="embla"> <div class="embla__viewport" use:useEmblaCarousel={{ options }} on:emblainit={onInit} > <div class="embla__container" id={carouselId}> {#each slides as slide} <div class="embla__slide">{slide}</div> {/each} </div> </div></div>