GodUIGodUI
129Follow on X

Anatomy of the GodUI Carousel

Why Embla already scrolls on the compositor, what GodUI leaves alone, and how reduced motion jumps instead of scrolling.

01/03Embla already moves the GPU way
tsx
<div data-slot="carousel-content" ref={carouselRef}className="overflow-hidden"><div className="flex -ml-4">  {/* Embla moves this one */}  <div data-slot="carousel-item" …/></div></div>/* each frame, from Embla: */transform: translate3d(-312px, 0px, 0px)
01

Embla already moves the GPU way

Carousels are usually where layout animation sneaks in: a track whose left or margin is tweened, so the browser lays out every slide on every frame.

Embla doesn't do that. The slides sit in a flex row inside an overflow-hidden viewport, and one inner element carries the movement as transform: translate3d(...), rewritten each animation frame. That's a compositor property, so scrolling runs without layout or paint. The same goes for dragging and for the settle after a flick, which is Embla's own spring physics rather than a CSS easing.

So GodUI leaves it alone. The scene advances on a timer, using the same scrollNext the buttons call.

02/03Reduced motion jumps
tsx
const reduce = usePrefersReducedMotion();const scrollPrev = React.useCallback(() => {api?.scrollPrev(reduce);}, [api, reduce]);const scrollNext = React.useCallback(() => {api?.scrollNext(reduce);}, [api, reduce]);
02

Reduced motion jumps

A slide that sweeps across the whole width is a lot of movement for someone who asked for less. Embla's scrollPrev and scrollNext take a jump argument: pass true and the carousel lands on the target slide in a single frame, with no scroll in between.

usePrefersReducedMotion is a small useSyncExternalStore over matchMedia("(prefers-reduced-motion: reduce)"). It reads false on the server and on the first client render, then follows the setting live, so changing it while the page is open takes effect on the next press. The arrow keys use the same two callbacks as the buttons. The scene above always jumps.

03/03Result
1
2
3
4
5
03

The result

Press the arrows, focus the carousel and use the arrow keys, or drag a slide. Switch on reduced motion in your system settings and the buttons and keys jump instead.

What's animated

InteractionKeyframe / mechanismPropertiesEasingDuration
SlideEmblatransform (translate3d)Embla's physicsuser-driven
Previous / next presstransitionscaleease-spring-bouncy260ms

Why GPU-only

Embla's per-frame update is a translate3d on one element, so scrolling never triggers layout or paint. A motion trace of a Next press in Chrome confirms no layout beyond the first frame, for both the horizontal and the vertical carousel. GodUI adds no transition of its own to the track.

Reduced motion

The previous/next buttons and the arrow keys pass true for Embla's jump argument, so the carousel lands on the slide without scrolling. Dragging is the user's own movement and stays as it is.

Replacing shadcn

npx shadcn add @godui/carousel overwrites components/ui/carousel.tsx. Exports, props and data-slot attributes match shadcn/ui new-york-v4, so existing imports keep working. It also installs godui-motion (easings, keyframes, the FLIP hook) into your project and the GodUI button the previous/next controls are built on.

The only change in behaviour: under prefers-reduced-motion the buttons and the arrow keys call Embla with jump, so the carousel lands on the next slide without scrolling. api (from setApi) is Embla's untouched API, so your own api.scrollTo(...) calls scroll as you wrote them.