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.
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.
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>
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.
Building it with the Interactions panel
no code, free planWhat 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.
Custom attributes
Element settings → Custom attributesEvery 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.
CSS
Page settings → Inside <head> tagThe script
Page settings → Before </body> tag<!-- 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>
Gotchas that will cost you an hour
- 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.
- 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.
- InertiaPlugin.track() before getVelocity(). Untracked, velocity is always 0 and the skew silently never happens — no error, just a slider that feels flat.
- 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.
- user-select: none on the cards. Otherwise a horizontal drag across text selects it, and you get a blue smear following the cursor.
- 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.
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