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 installing the SSR plugin package:
<script src="https://unpkg.com/embla-carousel-ssr/embla-carousel-ssr.umd.js"></script>npm install embla-carousel-ssr --savepnpm add embla-carousel-ssryarn add embla-carousel-ssrNext, ensure 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;}Then, enable SSR support by adding the Ssr plugin and generating server-side styles using the getStyles method.
You'll specify slide widths as percentages (%), exactly matching your CSS:
// SERVERimport EmblaCarousel from 'embla-carousel'import Ssr from 'embla-carousel-ssr'
export function getEmblaCarouselSsr(slides, carouselId) { const emblaServerApi = EmblaCarousel( null, { loop: true }, [Ssr({ slideSizes: slides.map(() => 50) })] // Each slide is 50% of the viewport width )
return ` <style> ${emblaServerApi .plugins() .ssr?.getStyles(`#${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 })import React, { useId } from 'react'import useEmblaCarousel from 'embla-carousel-react'import Ssr from 'embla-carousel-ssr'
export function EmblaCarousel(props) { const carouselId = useId() const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel( { loop: true }, [Ssr({ slideSizes: props.slides.map(() => 50) })] // Each slide is 50% of the viewport width ) const renderSsrStyles = !emblaApi
return ( <> {renderSsrStyles && ( <style> {emblaServerApi .plugins() .ssr?.getStyles(`#${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'import Ssr from 'embla-carousel-ssr'
const props = defineProps({ carouselId: String, slides: { type: Array, required: true }})
const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel( { loop: true }, [Ssr({ slideSizes: props.slides.map(() => 50) })] // Each slide is 50% of the viewport width)const renderSsrStyles = computed(() => !emblaApi.value)
const ssrStyles = ` <style> ${emblaServerApi .plugins() .ssr?.getStyles(`#${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'import Ssr from 'embla-carousel-ssr'
export function EmblaCarousel(props) { const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel( () => ({ loop: true }), () => [Ssr({ slideSizes: props.slides.map(() => 50) })] // Each slide is 50% of the viewport width )
const renderSsrStyles = () => !emblaApi()
return ( <> {renderSsrStyles() && ( <style> {emblaServerApi .plugins() .ssr?.getStyles(`#${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' import Ssr from 'embla-carousel-ssr'
export let slides export let carouselId
const options = { loop: true } const plugins = [Ssr({ slideSizes: slides.map(() => 50) })] // Each slide is 50% of the viewport width
let emblaApi const emblaServerApi = useEmblaCarousel({ options, plugins })
$: renderSsrStyles = !emblaApi const ssrStyles = ` <style> ${emblaServerApi.plugins().ssr?.getStyles(`#${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, plugins }} 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 slideSizes 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
slideSizes. - 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
slideSizes 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 slideSizes 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'import Ssr from 'embla-carousel-ssr'
export function getEmblaCarouselSsr(slides, carouselId) { const emblaServerApi = EmblaCarousel(null, { loop: true }, [ Ssr({ slideSizes: slides.map(() => 50), // Each slide is 50% of the viewport width breakpoints: { '(min-width: 768px)': { slideSizes: slides.map(() => 70) // Each slide is 70% of the viewport width } } }) ])
return ` <style> ${emblaServerApi .plugins() .ssr?.getStyles(`#${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 })import React, { useId } from 'react'import useEmblaCarousel from 'embla-carousel-react'import Ssr from 'embla-carousel-ssr'
export function EmblaCarousel(props) { const carouselId = useId() const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel( { loop: true }, [ Ssr({ slideSizes: props.slides.map(() => 50), // Each slide is 50% of the viewport width breakpoints: { '(min-width: 768px)': { slideSizes: props.slides.map(() => 70) // Each slide is 70% of the viewport width } } }) ] ) const renderSsrStyles = !emblaApi
return ( <> {renderSsrStyles && ( <style> {emblaServerApi .plugins() .ssr?.getStyles(`#${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'import Ssr from 'embla-carousel-ssr'
const props = defineProps({ carouselId: String, slides: { type: Array, required: true }})
const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel({ loop: true }, [ Ssr({ slideSizes: props.slides.map(() => 50), // Each slide is 50% of the viewport width breakpoints: { '(min-width: 768px)': { slideSizes: props.slides.map(() => 70) // Each slide is 70% of the viewport width } } })])const renderSsrStyles = computed(() => !emblaApi.value)
const ssrStyles = ` <style> ${emblaServerApi .plugins() .ssr?.getStyles(`#${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'import Ssr from 'embla-carousel-ssr'
export function EmblaCarousel(props) { const [emblaRef, emblaApi, emblaServerApi] = useEmblaCarousel( () => ({ loop: true }), () => [ Ssr({ slideSizes: props.slides.map(() => 50), // Each slide is 50% of the viewport width breakpoints: { '(min-width: 768px)': { slideSizes: props.slides.map(() => 70) // Each slide is 70% of the viewport width } } }) ] )
const renderSsrStyles = () => !emblaApi()
return ( <> {renderSsrStyles() && ( <style> {emblaServerApi .plugins() .ssr?.getStyles(`#${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' import Ssr from 'embla-carousel-ssr'
export let slides export let carouselId
const options = { loop: true } const plugins = [ Ssr({ slideSizes: slides.map(() => 50), // Each slide is 50% of the viewport width breakpoints: { '(min-width: 768px)': { slideSizes: slides.map(() => 70) // Each slide is 70% of the viewport width } } }) ]
let emblaApi const emblaServerApi = useEmblaCarousel({ options, plugins })
$: renderSsrStyles = !emblaApi const ssrStyles = ` <style> ${emblaServerApi.plugins().ssr?.getStyles(`#${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, plugins }} on:emblainit={onInit} > <div class="embla__container" id={carouselId}> {#each slides as slide} <div class="embla__slide">{slide}</div> {/each} </div> </div></div>