GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Carousel Slider UI Design — 9 Transitions

Among nine carousel slider transitions, one glides on from inertia as a card advances, one snaps back like a rubber band at the edge, and one bends cards in 3D

Auto-plays · click a tile to jump to its section · all nine in one zip

Nine is enough to cover the real spread of carousel slider ui design without repeating the same idea twice: three read pointer velocity into a physics loop, three compute a 3D-adjacent value per card per frame, and three sit at the edges of that split — a seam-free loop, a synced thumbnail strip, and a timer-driven autoplay. The order below moves from the most hands-on pattern (drag momentum) to the calmest one (a crossfade that barely moves at all).

Demo

Carousel That Keeps Gliding From Drag Momentum
Open in a new tab

01 · Carousel That Keeps Gliding From Drag Momentum — Measured area 27.174% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

onPointerMoverequestAnimationFramereleaseVelocity

Drag the card above, let it go mid-swipe, and watch how far it keeps sliding before it settles — that overshoot-then-stop is the one layer of math every pattern in this set adds on top of a plain transition.

Anatomy of nine transitions — where the one layer of calculation runs

Every pattern below adds one extra layer of computation on top of a plain CSS transform, and that layer is what turns a generic carousel slider design into nine transitions that each feel distinct.

# Pattern Cumulative area Avg. intensity Frames moved
01 Drag momentum 27.2% 174.3 23 of 23
02 Rubber-band edge 34.8% 132.2 23 of 23
03 Coverflow perspective 19.7% 76.6 22 of 23
04 Infinite-loop wrap 34.8% 145.9 9 of 23
05 Autoplay pause-on-hover 34.8% 108.8 20 of 23
06 Thumbnail sync 33.9% 55.5 22 of 23
07 Snap points 34.8% 129.1 18 of 23
08 Vertical stack 40.4% 84.2 18 of 23
09 Fade crossfade depth 34.8% 58.0 22 of 23

Three patterns — drag momentum (01), rubber-band edge (02), and snap points (07) — read pointer velocity from a short rolling window of recent samples and feed that single number into a physics loop (settle(), springBackIfNeeded(), or the snap-then-animateTo() pair) that keeps running inside requestAnimationFrame until it decays below a stop threshold.

Three more — coverflow (03), vertical stack (08), and crossfade depth (09) — compute a 3D-adjacent value (angle, translateZ, scale, or opacity) per card, per frame, from how far that card sits from the active index, which is why their motion feels continuous rather than stepped.

The remaining three sit at the edges of the group: infinite-loop wrap (04) has the lowest frame count because its wrapIfNeeded() reset only fires once a transition finishes, not every frame; thumbnail sync (06) eases a secondary strip rather than the main card; and autoplay pause-on-hover (05) is driven by elapsed time instead of a drag at all.

Code per pattern

Every pattern below keeps its full JavaScript source in view — copy the fence, paste it next to your own markup, and swap in your own card content.

01Carousel That Keeps Gliding From Drag Momentum

Drag a card sideways and let go, and it keeps sliding at the speed it was released, slowing gradually until it stops — the harder the flick, the farther it travels. pushSample() records recent pointer positions in a rolling window, and releaseVelocity() turns that window into a starting speed the moment you lift your finger. That single number then decays every frame inside settle() until it's small enough to stop.

Carousel That Keeps Gliding From Drag Momentum
Open in a new tab

01 · Carousel That Keeps Gliding From Drag Momentum — Measured area 27.174% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

onPointerMoverequestAnimationFramereleaseVelocity
  function settle() {
    var last = performance.now();
    function frame(now) {
      var dt = Math.min(40, now - last);
      last = now;
      pos = clamp(pos + velocity * dt, minX, maxX);
      var decay = Math.pow(1 - MOMENTUM_FRICTION, dt / STEP_MS);
      velocity *= decay;
      if (pos === minX || pos === maxX) velocity = 0;
      paint();
      if (Math.abs(velocity) > VELOCITY_STOP) {
        settleFrame = requestAnimationFrame(frame);
      } else {
        velocity = 0;
        settleFrame = null;
      }
    }
    settleFrame = requestAnimationFrame(frame);
  }

02Carousel That Snaps Back Like a Rubber Band at Its Edges

Push past the first or last card and it stretches a little instead of stopping dead, then snaps back the instant you let go. rawToVisual() compresses any distance past the edge through a rubber() curve so the resistance grows the farther you pull, and springBackIfNeeded() pulls the card back with a force-and-damping loop once you release it. It's the same edge feedback a phone's home screen gives you when you scroll past the last icon.

Carousel That Snaps Back Like a Rubber Band at Its Edges
Open in a new tab

02 · Carousel That Snaps Back Like a Rubber Band at Its Edges — Measured area 34.824% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

rawToVisualspringBackIfNeededonPointerMove
  // Spring-back — only when pos has crossed a boundary does it pull toward the target (nearest boundary) with force and damping.
  function springBackIfNeeded() {
    var target = pos > maxX ? maxX : (pos < minX ? minX : null);
    if (target === null) return;
    var velocity = 0;
    function frame() {
      var force = (target - pos) * SPRING_STIFFNESS;
      velocity = (velocity + force) * SPRING_DAMPING;
      pos += velocity;
      paint();
      if (Math.abs(velocity) > SPRING_STOP || Math.abs(target - pos) > SPRING_STOP) {
        springFrame = requestAnimationFrame(frame);
      } else {
        pos = target;
        paint();
        springFrame = null;
      }
    }
    springFrame = requestAnimationFrame(frame);
  }

03Carousel Whose Cards Curve Past in 3D Perspective

The center card stands tall and faces forward while the cards on either side tilt and shrink into the distance, curving the whole row as it turns. render() recalculates each card's angle, depth, and scale from the live drag distance every frame, which is a different technique from a scroll-snap-only coverflow that never touches an angle in JavaScript. That per-frame math is what a portfolio or album-cover browser needs to feel continuous instead of stepped.

Carousel Whose Cards Curve Past in 3D Perspective
Open in a new tab

03 · Carousel Whose Cards Curve Past in 3D Perspective — Measured area 19.651% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

animateIndexTotranslateZrotateY
  function render() {
    for (var n = 0; n < cards.length; n++) {
      var dist = n - index;
      var absDist = Math.abs(dist);
      var angle = clamp(dist * ANGLE_STEP, -MAX_ANGLE, MAX_ANGLE);
      var z = -absDist * DEPTH_STEP;
      var scale = Math.max(SCALE_FLOOR, 1 - absDist * SCALE_STEP);
      var opacity = Math.max(OPACITY_FLOOR, 1 - absDist * OPACITY_STEP);
      var x = dist * 58;
      cards[n].style.transform = 'translateX(' + x.toFixed(1) + 'px) translateZ(' + z.toFixed(1) + 'px) rotateY(' + angle.toFixed(1) + 'deg) scale(' + scale.toFixed(3) + ')';
      cards[n].style.opacity = opacity.toFixed(2);
      cards[n].style.zIndex = String(100 - Math.round(absDist * 10));
    }
    var shown = clamp(Math.round(index), 0, TOTAL - 1);
    readout.textContent = LABEL_PREFIX + (shown + 1) + LABEL_SEP + TOTAL;
  }

04Carousel That Wraps Seamlessly Back to the Start

A copy of the first card sits right after the last one, so the instant the boundary is crossed, wrapIfNeeded() quietly resets the position to the matching real card without a visible jump. That clone-plus-modulo trick is what makes a logo strip or brand row loop forever instead of visibly snapping back to card one. onTransitionEnd() is what times that invisible reset to the exact moment the CSS transition finishes, not a moment before.

Carousel That Wraps Seamlessly Back to the Start
Open in a new tab

04 · Carousel That Wraps Seamlessly Back to the Start — Measured area 34.824% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

wrapIfNeededonTransitionEndrealIndex
  function paint(withTransition) {
    track.style.transition = withTransition ? 'transform ' + TRANSITION_MS + 'ms cubic-bezier(.22,.61,.36,1)' : 'none';
    track.style.transform = 'translate3d(' + (-pos * STEP).toFixed(1) + 'px,0,0)';
    readout.textContent = LABEL_PREFIX + (realIndex() + 1) + LABEL_SEP + TOTAL;
  }

  // Modulo wrap — the instant a transition ends, if the position has reached a clone zone (before the first real card or past the last),
  // turn the transition off and jump pos to the matching real card in the same picture (invisible on screen).
  function wrapIfNeeded() {
    if (pos < CLONE_COUNT) {
      pos += TOTAL;
      paint(false);
      void track.offsetWidth; // force a reflow to lock in transition:none
    } else if (pos >= CLONE_COUNT + TOTAL) {
      pos -= TOTAL;
      paint(false);
      void track.offsetWidth;
    }
  }

  function onTransitionEnd() {
    transitioning = false;
    wrapIfNeeded();
  }
  track.addEventListener('transitionend', onTransitionEnd);

05Carousel That Autoplays and Pauses on Hover

Every set interval the carousel advances on its own while a thin bar fills up over that time, and resting the cursor on a card simply freezes the bar exactly where it was. tick() only adds elapsed time while playing is true and the pointer isn't hovering, so resuming continues the bar from the paused point rather than restarting the countdown. It's the pattern a promo-banner carousel slideshow needs so nobody loses their place mid-read.

Carousel That Autoplays and Pauses on Hover
Open in a new tab

05 · Carousel That Autoplays and Pauses on Hover — Measured area 34.826% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

showSlideonMouseEnterprogressFill
  function advance() {
    current = (current + 1) % TOTAL;
    showSlide(current);
    elapsed = 0;
  }
  function setPlayingUI() {
    playBtn.setAttribute('aria-pressed', playing ? 'false' : 'true');
    playBtn.setAttribute('aria-label', playing ? PAUSE_LABEL : RESUME_LABEL);
    iconPause.classList.toggle('isHidden', !playing);
    iconPlay.classList.toggle('isHidden', playing);
  }

  // The one calculated layer — each frame adds dt to elapsed, but skips the add while hovering (or !playing).
  // So when it resumes, the bar keeps filling from where it stopped, not from zero (the remaining time is preserved).
  function tick(now) {
    if (!lastTick) lastTick = now;
    var dt = Math.min(48, now - lastTick);
    lastTick = now;
    if (playing && !hovering && !reduce) {
      elapsed += dt;
      if (elapsed >= AUTOPLAY_MS) { advance(); }
    }
    progressFill.style.width = (Math.min(1, elapsed / AUTOPLAY_MS) * 100).toFixed(1) + '%';
    rafId = requestAnimationFrame(tick);
  }

06Carousel Whose Thumbnail Strip Highlights the Active Card

Each time the large card changes, only the matching thumbnail below brightens its border while the rest dim, so the current position is obvious at a glance. computeTargetOffset() works out how far the strip needs to shift so the active thumbnail sits centered, and centerActiveThumb() eases the strip toward that offset frame by frame — an always-centered layer a simple active-class toggle doesn't add on its own. That's what keeps a long thumbnail row readable deep into a product gallery.

Carousel Whose Thumbnail Strip Highlights the Active Card
Open in a new tab

06 · Carousel Whose Thumbnail Strip Highlights the Active Card — Measured area 33.855% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

computeTargetOffsetcenterActiveThumbthumbCenter
  function centerActiveThumb() {
    targetOffset = computeTargetOffset();
    stopCenter();
    function frame() {
      offset += (targetOffset - offset) * CENTER_EASE;
      thumbTrack.style.transform = 'translate3d(' + offset.toFixed(1) + 'px,0,0)';
      if (Math.abs(targetOffset - offset) > CENTER_STOP) {
        centerFrame = requestAnimationFrame(frame);
      } else {
        offset = targetOffset;
        thumbTrack.style.transform = 'translate3d(' + offset.toFixed(1) + 'px,0,0)';
        centerFrame = null;
      }
    }
    centerFrame = requestAnimationFrame(frame);
  }

07Carousel That Always Settles Exactly on One Card

Even a release between two cards glides the rest of the way to the nearest edge, so exactly one card always ends up aligned to the screen. releaseVelocity() reads how fast and which way the drag was moving, and judgeSnapIndex() uses that direction to decide whether to settle on the card you were leaving or the one you were heading toward. A slideshow where a half-visible card would look broken needs exactly this kind of direction-aware snap.

Carousel That Always Settles Exactly on One Card
Open in a new tab

07 · Carousel That Always Settles Exactly on One Card — Measured area 34.805% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

judgeSnapIndexreleaseVelocityanimateTo
  function judgeSnapIndex(rawPos, velocity) {
    var rawIndex = -rawPos / step;
    var idx = Math.round(rawIndex);
    if (Math.abs(velocity) > SNAP_VELOCITY_BIAS) {
      idx += velocity < 0 ? 1 : -1;
    }
    return clamp(idx, 0, TOTAL - 1);
  }

  function pushSample(t, x) {
    samples.push({ t: t, x: x });
    while (samples.length && t - samples[0].t > SAMPLE_WINDOW_MS) samples.shift();
  }
  function releaseVelocity() {
    if (samples.length < 2) return 0;
    var a = samples[0], b = samples[samples.length - 1];
    var dt = b.t - a.t;
    if (dt <= 0) return 0;
    return (b.x - a.x) / dt;
  }

08Carousel That Stacks Cards Vertically as It Advances

Cards stack top to bottom instead of side to side, and the next one rises from below to slightly overlap the card in front as it takes over. onCardClick() only advances when the front-most card itself is clicked, while onWheel() reads scroll direction with a short cooldown so one swipe doesn't fire five transitions at once. It suits a tall, mobile-first stack of story or announcement cards better than a side-scrolling row would.

Carousel That Stacks Cards Vertically as It Advances
Open in a new tab

08 · Carousel That Stacks Cards Vertically as It Advances — Measured area 40.394% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

onWheelonCardClickzIndex
  function onCardClick(e) {
    var el = e.target.closest('.card');
    if (!el || cards.indexOf(el) !== order[0]) return; // only advance when the front-most card was clicked
    human();
    rotate(1);
  }
  function onWheel(e) {
    var now = performance.now();
    if (now - lastWheel < WHEEL_COOLDOWN_MS) { e.preventDefault(); return; }
    lastWheel = now;
    human();
    rotate(e.deltaY > 0 ? 1 : -1);
    e.preventDefault();
  }

09Carousel That Crossfades Cards Back Into Depth

While the next card fades in sharply, the previous one fades out and shrinks slightly, as if it's sinking back into the distance in the same spot. easeInOutCubic() shapes that motion so it starts and ends gently instead of linearly, and paintStatic() draws the same opacity-scale-depth combination instantly whenever there's no animation to run, including on first load. That shared math is why the transition looks identical whether it's animating or just being drawn once.

Carousel That Crossfades Cards Back Into Depth
Open in a new tab

09 · Carousel That Crossfades Cards Back Into Depth — Measured area 34.82% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

easeInOutCubictranslateZpaintStatic
  // Computes the opacity, scale, and translateZ values together.
  function easeInOutCubic(p) {
    return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
  }

  function paintStatic() {
    cards.forEach(function (el, n) {
      var active = n === current;
      el.style.opacity = active ? '1' : '0';
      el.style.transform = active ? 'translateZ(0) scale(1)' : 'translateZ(' + (-DEPTH_PUSH) + 'px) scale(' + (1 - DEPTH_SCALE).toFixed(3) + ')';
      el.style.zIndex = active ? '2' : '1';
    });
    dots.forEach(function (el, n) { el.classList.toggle('isActive', n === current); });
    readout.textContent = LABEL_PREFIX + (current + 1) + LABEL_SEP + TOTAL;
  }

Where values change the UI — inertia decay, bounce strength, autoplay interval

01 — MOMENTUM_FRICTION

Lowering MOMENTUM_FRICTION from 0.10 to 0.05 keeps the card gliding longer and farther after release — the decay settle() applies every frame gets gentler, so momentum survives more frames before VELOCITY_STOP cuts it off.

05 — AUTOPLAY_MS

Narrowing AUTOPLAY_MS from 1350 to 900 makes tick() reach the interval sooner every cycle, advancing cards more often and making the whole sequence feel faster.

07 — SNAP_EASE

Lowering SNAP_EASE from 0.24 to 0.12 makes the card's move into place after release look more leisurely and take longer, since each frame closes a smaller fraction of the remaining distance.

Where it breaks — accessibility, reduced-motion, touch-swipe conflicts

All nine demos check prefers-reduced-motion once at load, but reduced motion only ever switches off the page's own demoCycle() autoplay loop that plays the transitions for a visitor who never touches the carousel — the moment a visitor actually drags, clicks, or presses an arrow key, the same velocity, spring, or snap math the demo used runs in full, because that's the real interaction path they came for. Every drag also has to share the pointer with the browser's own touch-scroll gesture, which is why each demo calls setPointerCapture() and preventDefault() on the axis it owns instead of fighting the whole page — leave that call out and a horizontal drag turns into a fight with the page's vertical scroll on a phone.

  • 01 drag momentum — reduced motion has no effect on the drag itself; only the automatic demo cycle stops.
  • 02 rubber-band edge — the spring-back math still runs on every real drag past an edge.
  • 03 coverflow perspective — the angle and depth recalculation lives in render(), not the demo loop, so it stays on.
  • 04 infinite-loop wrap — wrapIfNeeded() keeps firing on every real transition end regardless of the setting.
  • 05 autoplay pause-on-hover — the one exception: playing = !reduce turns the autoplay engine itself off from the start, because continuous unattended motion is the real vestibular concern here, not just the demo.
  • 06 thumbnail sync — centerActiveThumb() still eases the strip on every real click or arrow press.
  • 07 snap points — the snap-to-nearest-card math still runs after every real drag release.
  • 08 vertical stack — click and wheel navigation stack the cards the same way either way.
  • 09 fade crossfade depth — the dot navigation uses role="tablist", and paintStatic(), the reduced-motion-safe path, is the same function the real UI calls on first load.

Every demo's full source — HTML, CSS, and JS for all nine patterns — sits in the download behind qswujuq5, so any one pattern can drop straight into a project without retyping it from the fence above. All nine also share the same baseline wiring: the carousel itself carries role="region" and aria-roledescription="carousel", the previous and next buttons each get their own aria-label, and the readout text sits in an aria-live="polite" region so a screen reader announces every card change without extra focus handling.

Three variants and accessibility

Variant Changed value What it feels like
Lower the inertia decay constant 01 MOMENTUM_FRICTION 0.10 → 0.05 Lowering MOMENTUM_FRICTION from 0.10 to 0.05 keeps the card gliding longer and farther after release.
Narrow the autoplay interval 05 AUTOPLAY_MS 1350 → 900 Narrowing AUTOPLAY_MS from 1350 to 900 advances cards more often, making the whole sequence feel faster.
Extend the snap-settle duration 07 SNAP_EASE 0.24 → 0.12 Lowering SNAP_EASE from 0.24 to 0.12 makes the card's move into place after release look more leisurely and take longer.

Each variant is a single constant, so testing a different feel is a one-line edit rather than a rewrite.

Where can I see carousel slider examples?

Beyond this set of nine, the carousel slider examples worth studying tend to share one habit: the motion is computed from a real input — pointer velocity, elapsed time, or drag distance — instead of a single CSS transition firing on a class toggle. That's what separates a demo that merely looks animated from one that actually responds to how a visitor drags, waits, or clicks.

FAQ

What's the difference between a carousel slideshow and a plain image slider?

A plain slider usually swaps images on a class toggle and a single CSS transition. Each pattern in this set adds one working layer underneath that transition — momentum, a spring, or elapsed time — so the motion responds to how it was triggered instead of always looking identical.

Why does one card glide farther than another after I let go of a carousel slider?

Because the release speed is different. releaseVelocity() measures how far the pointer moved in the last few samples before you lifted your finger, and a faster release starts settle() with a larger number, which takes more frames to decay below the stop threshold.

Does a carousel slider need a library like Swiper or Slick?

No — every pattern here runs on plain requestAnimationFrame, Pointer Events, and CSS transforms with no external script. That's also why the whole nine-pattern set stays far under the file size a single slider library alone would already cost.

Sources: Window.requestAnimationFrame() · Pointer events · Using CSS transforms · prefers-reduced-motion · perspective

Related reading: 9 CSS 3D Cube Effects — One Line, Copy-Paste · CSS Image Slider — 9 Responsive Sliders, No GSAP · 9 Before After Slider UIs — Drag to Compare

Enter the archive password

The password is inside this article. You will find it as you read.