L3

·

Drag · physics

Draggable inertia slider

Swiper and Flickity give you a slider in ten minutes and it always feels like a slider. This feels like an object, and the reason is InertiaPlugin: when you release, the track decelerates on measured velocity rather than easing to a preset position over a preset duration. Two additions finish it. Snapping that runs through the physics rather than instead of it — the throw still happens, it just resolves onto a card edge. And velocity-driven skew, clamped hard at eleven degrees, which is the whole “Awwwards slider” feel and is four lines. Both plugins are free now; this used to be the paid tier.

Open the live demo →

How to build

Script only

Free plan gets you part of the way

The panel covers some of this. The rest needs the script, which needs a paid plan — build the panel half now, come back for the rest.

How the two routes work →

Plugins

Draggable · InertiaPlugin · Observer

Build time

1 hour. Bounds and resize are the fiddly parts.

No-code?

No. Webflow's slider component has no physics and no velocity. This is a code component — but the markup is plain Webflow divs, so it stays fully editable in the Designer.

01

Webflow structure

element · class · attribute

<b>Div</b> <i>slider_viewport</i> ← overflow hidden, cursor grab <b>Div</b> <i>slider_track</i> ← display flex, gap, will-change transform <b>Collection Item</b> <i>slider_card</i> ← flex: none + explicit width <b>Div</b> <i>slider_card-frame</i> ← overflow hidden, fixed aspect-ratio <b>Image</b> ← pointer-events: none, draggable false <b>Heading H3</b> <b>Text</b> <i>slider_card-meta</i> <b>Div</b> <i>slider_hud</i> <b>Text</b> <i>slider_hint</i> <b>Div</b> <i>slider_rail</i> <b>Div</b> <i>slider_bar</i>

02

Styles to set in the Designer

slider_viewport | <code>overflow: hidden</code> and <code>cursor: grab</code>. Do NOT set <code>overflow-x: auto</code> — native scrolling and Draggable fight over the same gesture. slider_track | <code>display: flex</code> with a gap and horizontal padding. No width — it must size to its children so <code>scrollWidth</code> is real. slider_card | <code>flex: none</code> and a clamped width. Also <code>user-select: none</code>, or a drag turns into a text selection halfway across the row. slider_card img | <code>pointer-events: none</code> plus the <code>draggable="false"</code> attribute. Browsers native-drag images by default and it hijacks the gesture instantly.

03

Building it with the Interactions panel

no code, free plan

What the panel still can’t do. Only custom code can close these gaps — there is no panel setting for any of them.

See “The script” below for the exact code.

04

Custom attributes

Element settings → Custom attributes

Every image in the slider | draggable | false | The browser's built-in image drag beats Draggable to the event. One attribute, and it is the most common “why does it stop halfway” bug.

05

CSS

Page settings → Inside <head> tag

Inside <head> tag

Copy

06

The script

Page settings → Before </body> tag

Before </body> tag

Copy

<!-- Page settings → Before </body> tag --> <script> document.addEventListener("DOMContentLoaded", () => { gsap.registerPlugin(Draggable, InertiaPlugin, Observer); const viewport = document.querySelector(".slider_viewport"); const track = document.querySelector(".slider_track"); const bar = document.querySelector(".slider_bar"); const cards = gsap.utils.toArray(".slider_card"); // ── measurements live in a function, re-run on resize ─────────────── let minX = 0, points = []; function measure() { minX = Math.min(0, viewport.offsetWidth - track.scrollWidth); points = cards.map(c => gsap.utils.clamp(minX, 0, -c.offsetLeft + track.offsetLeft)); } measure(); const setBar = gsap.quickSetter(bar, "x", "px"); const paint = () => { const x = gsap.getProperty(track, "x") || 0; setBar(minX === 0 ? 0 : (x / minX) * (bar.parentElement.offsetWidth * 0.78)); }; // Velocity tracking must be switched on explicitly before you can read it. InertiaPlugin.track(track, "x"); const skew = gsap.quickTo(cards, "skewX", { duration: 0.5, ease: "power3" }); function tick() { const v = InertiaPlugin.getVelocity(track, "x"); skew(gsap.utils.clamp(-11, 11, -v / 260)); // clamp HARD. 11deg, not 30. paint(); } const [drag] = Draggable.create(track, { type: "x", inertia: true, // the whole point cursor: "grab", activeCursor: "grabbing", allowNativeTouchScrolling: true, // vertical page scroll still works on a phone edgeResistance: 0.9, // rubber-band at the ends instead of a hard stop bounds: () => ({ minX, maxX: 0 }), // Snap runs THROUGH the throw, not instead of it: the physics still play // out, they just resolve onto a card edge. snap: { x: v => gsap.utils.snap(points, v) }, onDrag: tick, onThrowUpdate: tick, onThrowComplete: () => { skew(0); paint(); } }); // Trackpad / wheel — horizontal deltas only, so the page still scrolls. Observer.create({ target: viewport, type: "wheel", onChangeX: self => { const x = gsap.utils.clamp(minX, 0, (gsap.getProperty(track, "x") || 0) - self.deltaX); gsap.to(track, { x, duration: 0.6, ease: "power3.out", overwrite: true, onUpdate: tick }); } }); addEventListener("resize", () => { measure(); drag.applyBounds({ minX, maxX: 0 }); gsap.set(track, { x: gsap.utils.clamp(minX, 0, gsap.getProperty(track, "x") || 0) }); paint(); }); paint(); }); </script>

07

Gotchas that will cost you an hour

  1. draggable="false" on every image. The browser's native image drag fires first and the slider stops dead mid-gesture. This is the number one support question for every drag slider ever built.
  2. allowNativeTouchScrolling: true or you break phones. Without it Draggable swallows vertical swipes too, and the visitor cannot scroll past your slider. They will just leave.
  3. InertiaPlugin.track() before getVelocity(). Untracked, velocity is always 0 and the skew silently never happens — no error, just a slider that feels flat.
  4. Bounds must be a function. bounds: {minX: -1200} is correct until someone resizes, and then the last card is unreachable or the track drifts into empty space.
  5. user-select: none on the cards. Otherwise a horizontal drag across text selects it, and you get a blue smear following the cursor.
  6. Clamp the skew low. The instinct is 25–30 degrees because it looks dramatic in isolation. At real speed it turns the type to mush; 11 is the ceiling.
08

Push it further

  • Add keyboard support — left/right arrows tweening x to the neighbouring snap point. It costs eight lines and it is the difference between shipping and failing an audit.
  • Wire the drag cursor state from the cursor system to the viewport's hover. Together they read as one interaction model rather than two features.
  • For an infinite version, wrap x with gsap.utils.wrap() and reposition cards past the seam — that is the pattern the infinite canvas uses in two dimensions.

Webflow Motion Lab — GSAP 3.15, all plugins free.

24

/ 30