Appearance
Embla libraries comparison
How @rozie-ui/embla compares to the existing per-framework Embla Carousel wrappers. Embla is the de-facto dependency-free, library-agnostic carousel engine: its core is pure vanilla JS that attaches to a viewport, reads the consumer's slide DOM, and drives transform: translate3d(...). Every framework wrapper exists only to glue reactive state to that imperative engine, surface its options as props, and forward the event set. The result is a lopsided ecosystem: four official wrappers with four divergent APIs, a single-maintainer Angular community package, and nothing at all for Lit / web components. Rozie ships the same <Carousel>, with the same props, events, two-way selectedIndex, and imperative handle, to all six frameworks as pre-compiled per-framework packages.
Research snapshot: 2026-08-10. Versions and the wrapper landscape move; treat them as of that date. All six Rozie packages wrap Embla v8 (
embla-carousel@^8.6); Embla v9 is RC-only and renames the API surface, so it is deliberately not targeted yet.
The wrappers at a glance
| Framework | Embla wrapper | Shape | Depth | Notes |
|---|---|---|---|---|
| React | embla-carousel-react | a hook (useEmblaCarousel) | deep | Official, maintained. Returns a [ref, api] tuple; you wire the ref onto the viewport and call the api yourself. |
| Vue | embla-carousel-vue | a composable | deep | Official. Returns a [ref, api] composable; Vue-idiomatic but a different surface from the React hook. |
| Svelte | embla-carousel-svelte | an action (use:emblaCarousel) | deep | Official. A Svelte action directive; again a different shape from hook/composable. |
| Solid | embla-carousel-solid | a Solid primitive | moderate | Official. A createEmblaCarousel primitive; a fourth divergent API. |
| Angular | embla-carousel-angular (community) | a directive | shallow | A single-maintainer community package, version-pinned to Angular majors; not first-party. |
| Lit / web components | (none) | — | — | No wrapper exists. You hand-roll the engine attach + lifecycle yourself. |
| Rozie | @rozie-ui/embla-* | a component | deep | Same API on all six: props, events, two-way selectedIndex, handle. |
On its home framework each official wrapper is a solid pick. The case for Rozie is consistency and coverage: the four official surfaces are a hook, a composable, an action, and a primitive, four different mental models for the same engine; Angular has only a community package; and Lit has nothing. A team shipping a cross-framework design system today maintains four divergent bindings plus a hand-rolled Angular/Lit story. Rozie gives them all the same <Carousel> component: the same props, events, two-way binding, and imperative handle.
Feature matrix
Cell legend: ✅ = documented out-of-the-box · ❌ = not supported / not present · ⚠️ = partial / consumer-glue-required.
| Capability | …-react | …-vue | …-svelte | …-solid | Angular (community) | Lit (none) | @rozie-ui/embla |
|---|---|---|---|---|---|---|---|
| Embla v8 engine | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Idiomatic component surface | ⚠️ hook | ⚠️ composable | ⚠️ action | ⚠️ primitive | ⚠️ directive | hand-roll | ✅ <Carousel> |
| Option surface as props | ⚠️ via options object | ⚠️ via options object | ⚠️ via options object | ⚠️ via options object | ⚠️ | — | ✅ 20 props |
| Two-way snap index | ⚠️ via select listener | ⚠️ via select listener | ⚠️ via select listener | ⚠️ via select listener | ⚠️ | — | ✅ selectedIndex model (echo-guarded) |
| Runtime option reconcile | ⚠️ manual reInit | ⚠️ manual reInit | ⚠️ manual reInit | ⚠️ manual reInit | ⚠️ | — | ✅ 13 of 20 props $watch→reInit (17 runtime-reactive in all) |
| Autoplay plugin toggle | ⚠️ wire the plugin yourself | ⚠️ | ⚠️ | ⚠️ | ⚠️ | — | ✅ autoplay prop |
| Imperative handle | ✅ via the api object | ✅ via the api object | ✅ via the api object | ✅ via the api accessor | ⚠️ | hand-roll | ✅ uniform 14-verb $expose |
| Plugin / options escape hatch | ✅ | ✅ | ✅ | ✅ | ⚠️ | — | ✅ :plugins + :options |
| Config-array and declarative slides | ❌ (slot only) | ❌ | ❌ | ❌ | ❌ | — | ✅ :slides array OR default slot, on all six targets¹ |
| TypeScript | ✅ | ✅ | ✅ | ✅ | ⚠️ | — | ✅ |
| Accessibility (carousel ARIA role, keyboard nav, labels) | ❓ not verified | ❓ not verified | ❓ not verified | ❓ not verified | ❓ not verified | — | ⚠️ arrow/dot aria-labels only — no carousel role, no keyboard nav on the viewport; fuller carousel a11y is on the roadmap |
| Zero-config styling, re-skinnable | ⚠️ unstyled, wire it | ⚠️ unstyled, wire it | ⚠️ unstyled, wire it | ⚠️ unstyled, wire it | ⚠️ unstyled, wire it | — | ✅ CSS-var tokens + shadcn/Material/Bootstrap bridges |
| Same API on all 6 frameworks | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
¹ Declarative (default-slot) mode resolves on Lit via the $slotted.<name> sigil, which reaches across the shadow boundary that a bare container.querySelectorAll('.rozie-embla__slide') cannot see through.
Where Rozie wins today
- First-class packages everywhere — including Lit, which has no Embla wrapper at all, and Angular, which has only a single-maintainer community package. Lit gets an Embla custom element; Angular gets a signals component from the same family as the other five.
- The same component surface everywhere. The four official wrappers are a hook, a composable, an action, and a primitive.
@rozie-ui/emblais one<Carousel>component with the same props, events, and handle: one component to learn, document, and migrate across your stack. - A real two-way snap index. The
selectedIndexmodel reads and drives the carousel: dragging echoes the new index out (via Embla'sselectevent), and a consumer write scrolls the carousel (echo-guarded so a programmaticscrollTodoesn't ping-pong). The official wrappers surface the index via a one-wayselectlistener; you wire the write-back yourself. - A uniform 14-verb imperative handle (
scrollNext/scrollPrev/scrollToIndex/reInitCarousel/canScrollNext/canScrollPrev/getSelectedIndex/scrollSnapList/scrollProgress/slidesInView/slidesNotInView/previousScrollSnap/getPlugins/getInstance) grabbed with each framework's native ref, identical on every target. - Two slide-source modes from one component. Pass a
:slidesconfig array, or drop declarative.rozie-embla__slidechildren into the default slot. The official wrappers only support consumer-authored slide DOM. getInstance()is always one hop from the raw engine, so the full Embla v8 API is reachable on any target when the curated surface doesn't cover something.- Zero-config styling that re-skins to any design system. Every rendered value is a
--rozie-embla-*CSS custom property with a built-in fallback, plus ready-made token bridges for shadcn/ui, Material 3, and Bootstrap 5. The official wrappers ship the bare engine unstyled and you wire arrows, dots, and thumbnails yourself; Rozie works on drop-in yet stays fully re-skinnable.
What Rozie defers
- React/Vue/Svelte/Solid depth on their home framework. The four official wrappers are mature, maintained, and shaped to their framework's own conventions (a React dev expects a hook; a Svelte dev expects an action). On its home framework each is a perfectly good pick. Rozie's value is the same component everywhere, with Lit and Angular getting an Embla component they otherwise lack. For anything outside the curated surface,
getInstance()hands you the raw engine on every target. - Per-option setters. Embla itself ships no per-option setters;
reInit(options)is the only update path. So@rozie-ui/embla$watches each runtime option and callsreInit, exactly as you would by hand with the official wrappers. A consumer rapidly mutating options can thrashreInit; debounce on the consumer side if needed (Embla's nativewatchSlidesis incremental for slide changes). @rozie-ui/emblais pre-1.0 and younger than the official wrappers. The full surface is documented in the showcase & API.- Embla v8, not v9. These packages wrap the mature v8. Embla v9 (RC at time of writing) renames the API surface; teams that want v9 are better served by it directly once it ships stable.
Try it
The @rozie-ui/embla showcase + API reference documents the @rozie-ui/embla-* packages: one pre-compiled, per-framework install (npm i @rozie-ui/embla-react embla-carousel embla-carousel-autoplay, etc.). There is no engine CSS to import: Embla's carousel skeleton ships scoped and fully-tokenised inside the component, with optional one-line theme bridges for shadcn/ui, Material 3, and Bootstrap 5. The showcase walks the two-way selectedIndex binding, the event surface, the imperative handle, theming, and the prev/next/dots recipe.
Cross-references
- Embla — showcase & API — the full
@rozie-ui/emblasurface, quick starts, theming, and recipes. - Embla — live demo — the real Vue package running in the page.
Carousel.roziesource on GitHub- Cropper libraries comparison — a sibling engine-wrapper port.