aboutusesTILblog
js27.02.2025 8 min read

Embla Carousel - a cursory look

A nice little carousel library to help you create components that I really wish didn't exist in 2025

An image of an ai generated carousel

I know right… a blog post about implementing an image (media) carousel? What is this, the early 2000s 😂?! Well it’s 2025 and I work for a commerce company so this is one case where actually having a media gallery totally makes sense — people want to see images of what they’re potentially going to buy before they buy it! While it’s true that we already have a media gallery implementation on our PDPs at Best Buy, the current implementation is lacking when it comes to accessibility standards — something that’s important to us folks working on the .ca website and well, should be important to anyone developing on the web these days!

Since we had landed on rewriting this component with a brand new design, we as developers also thought it was high time that we pick a new underlying library to power the media gallery experience. We landed on, and I’m sure you’re not surprised, Embla Carousel.

Here are some of the reasons why we opted for Embla — mind you that this list isn’t exhaustive:

  1. react-slick didn’t fit our needs anymore.
  2. shadcn uses Embla under the hood for their carousel
  3. Embla is headless and doesn’t require you to throw in any of their boilerplate css; it lets you choose your own adventure.

Reason number three is my favorite thing about Embla because it means you can choose whatever solution you want for your CSS; we of course leaned into tailwind for our stuff 😅.

Things I Wish I Had Brushed up on before Doing the Work

Really, the only thing I wish I had brushed up on is the flexbox layout algorithm. A lot of the the examples in the embla docs lean into flexbox (and why shouldn’t they, flexbox is great) for their layout algorithm of choice but since it had been a hot minute since I’d worked with flexbox, I felt a little out of my element. I honestly had a tougher time than necessary because I struggled so much with basic CSS; so much such that I’m actually writing a blog post about this problem and how I addressed it so keep your eyes peeled 👀 🍌.

Embla

So Embla Carousel is defined as being a lightweight carousel library. Beyond just that, the maintainers of the library have done a great job creating extensive documentation with a myriad of examples that I would say cover about 80% of the use cases that most people are to encounter in their day to day development.

In their predefined examples, they cover things such as lazy-loading, adding a thumbnail slider, and handling things like autoplay! Thanks to their plugin system, it also makes it possible for you to create your own custom plugin to handle a use case that may not exist in the current documentation (pretty slick y’all).

For us over at bestbuy.ca, we needed to handle lazy loading our assets (because we’re good people and don’t want to needlessly make network request for our users), and had our own custom thumbnail slider that kind of differed from what existed in the guide.

Beyond what I just said about the great docs, the fact that embla is headless, makes it so easy to lean naturally into reacts composition model. Check out the code below:

embla-example-media-gallery.tsx
const MediaGallery = ({ slides }: { slides: Array<Slide> }) => {
	const [selectedIndex, setSelectedIndex] = React.useState(0)
	const [slidesInView, setSlidesInView] = React.useState<number[]>([])
	const [emblaMainRef, emblaMainApi] = useEmblaCarousel({
		active: slides.length > 0,
		inViewThreshold: 0.5,
	})

	const onSelect = React.useCallback(() => {
		if (!emblaMainApi) return
		setSelectedIndex(emblaMainApi.selectedScrollSnap())
	}, [emblaMainApi, setSelectedIndex])

	return (
		<div className="relative m-auto flex max-w-3xl flex-wrap items-center sm:flex-nowrap">
			{/* Desktop Thumbnail that lives in another file */}
			<DesktopThumbnailSlider
				selectedIndex={selectedIndex}
				slides={slides}
				mainMediaSliderApi={emblaMainApi}
			/>
			{/* main slider api */}
			<div
				className="group relative order-1 overflow-hidden"
				ref={emblaMainRef}
			>
				<div className="flex max-h-[500px] max-w-[500px] touch-pan-x touch-pinch-zoom">
					{slides.map((obj, index) => (
						<div className="flex-[0_0_100%] px-4">
							<ProductMedia
								inView={slidesInView.indexOf(index) > -1}
								resolution="medium"
								key={index}
								obj={obj}
								index={index}
								selectedIndex={selectedIndex}
							/>
						</div>
					))}
				</div>
				<DesktopScrollButtons emblaApi={emblaMainApi} />
			</div>

			{/* mobile slider that shows up underneath and lives in a different file  */}
			<MobileThumbnailSlider
				selectedIndex={selectedIndex}
				totalSlides={slides.length}
				mainMediaSlidersApi={emblaMainApi}
			/>
		</div>
	)
}

Interesting Challenges

Ssr and ImageonLoad

So unsurprisingly, or maybe it is surprising — depends on who is reading this post and what their career experience is — we’ve sort of hand-rolled our own custom SSR framework! Since we’re server side rendering our react output, I ran into one interesting issue with the lazy loading of our image assets: onLoad was never called!

This is what I was doing before (and what I thought would just work) but it didn’t actually work 🤭:

const ImageType = ({
	inView,
	isFirstSlide,
}: {
	inView: boolean
	isFirstSlide: boolean
}) => {
	const [hasLoaded, setHasLoaded] = React.useState(false)
	const imgRef = React.useRef<HTMLImageElement>(null)

	const onLoad = React.useCallback(() => {
		if (inView || isFirstSlide) {
			setHasLoaded(true)
		}
	}, [inView, isFirstSlide])

	return <img onLoad={onLoad} />
}

After some digging, I found this Stack Overflow issue — surprise surprise, AI couldn’t help me and I had to go back to the OG — that basically said that the onLoad event wasn’t called because the image had already loaded! I had to make the following tweak to my above code:

Now this isn’t really an Embla flaw so please don’t hold it against the library!

const ImageType = ({
	inView,
	isFirstSlide,
}: {
	inView: boolean
	isFirstSlide: boolean
}) => {
	const [hasLoaded, setHasLoaded] = React.useState(false)
	const imgRef = React.useRef<HTMLImageElement>(null)

	const onLoad = React.useCallback(() => {
		if (inView || isFirstSlide) {
			setHasLoaded(true)
		}
	}, [inView, isFirstSlide])

	/**
	 * There was an issue where the `onLoad` event was not firing for our images when they were server side rendered.
	 * By attaching a ref to the host DOM node, we are able to wait and monitor for once the host node is completed
	 * and can fire our `onLoad` event.
	 *
	 * https://stackoverflow.com/questions/59143484/onload-event-of-img-tag-doest-fire-on-initial-page-access
	 */
	React.useEffect(() => {
		if (imgRef.current?.complete) {
			onLoad()
		}
	}, [onLoad])

	return <img ref={imgRef} {...props} />
}

Next and Previous Events

So I had a dickens of a time trying to figure out why the onNext and onPrevious functions that embla provides weren’t behaving consistently. This github issue kind of goes into it and has a solution that’s usable (and is being used by me) to get a more consistent next and previous behavior with my sliders.

There’s a disconnect between how the next and previous work out of the box with embla and how I wanted it to work but the below code (that’s reaching into the internalEngine) accomplishes what I’m looking for and probably what you’re looking for too. It does a check to see if there’s still room to scroll based on not the available screen real estate in the viewport for the slider but based on if there are any slides either before or after the current slide.

can-scroll-handlers
// gitub issue: https://github.com/davidjerleke/embla-carousel/issues/995
const canScrollBackward = (emblaApi: EmblaCarouselType) => {
        const {target, limit} = emblaApi.internalEngine();
        const targetRounded = parseFloat(target.get().toFixed(2));
        return targetRounded < limit.max;
    };

    const canScrollForward = (emblaApi: EmblaCarouselType) => {
        const {target, limit} = emblaApi.internalEngine();
        const targetRounded = parseFloat(target.get().toFixed(2));
        return targetRounded > limit.min;
    };

Would I Use Embla Again?

After working with Embla, would I use it again? Yes, but with caveats.

What I Liked:

What Frustrated Me:

Overall, if you need a flexible, accessible, and performant carousel, Embla is a solid choice—but expect to put in some effort upfront.

Closing Thoughts

Building carousels in 2025 feels like a cruel joke, but here we are.

Embla worked well for our needs, but it reinforced an important lesson: sometimes the hardest part of a project isn’t the library—it’s the fundamentals. My biggest struggle wasn’t Embla itself, but Flexbox, lazy loading quirks, and understanding how SSR affects browser events.

Would I have preferred to avoid this project altogether? Absolutely. Did I learn something useful? Also, yes. If you’re implementing a media gallery and want a lightweight, headless solution, Embla is worth considering—just be ready to fine-tune it.