Drag slider
A row of cards driven by the hand, with the mouse or a finger.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialGive the cards a width: the row is a flex line that never wraps, and a card with none shrinks to its words. And leave the track's transform to the engine, which writes it every frame. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Tune it
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the section on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-drag-slider Marks the section that holds the row. It takes no value: leave the value field empty. That section is what the engine mounts on, what carries the settings and the states, and what it looks for the viewport, the track and the counter in. Without it on any section of the page, the embed marks the section holding each row (data-drag-slider-track). data-drag-slider-viewport Marks the box that clips the row, the parent of the track, and the one the hand grabs. With the script it clips without scrolling and hands the vertical swipe to the page. Without it the parent of the track is taken. [ Optional ] data-drag-slider-track Marks the row that moves, the block holding the cards. Nothing is mounted without it, and the engine says so in the console. [ Required ] data-drag-slider-card Marks one card, a child of the track. Without it on any of them the engine takes the track's own children as the cards. [ Optional ] data-drag-slider-current Marks the number of the card the counter reads. The engine fills it with one rolling reel of digits per figure. Without it there is no current number. [ Optional ] data-drag-slider-total Marks the total of the counter, which the engine writes as the card count. Without it there is no total. [ Optional ] data-drag-slider-bar Marks the fill of the progress rule, which the engine grows from the left as the row goes. Without it there is no rule to fill. [ Optional ] data-drag-slider-state Written by the engine on the section: ready when the row is longer than its viewport, fit when it is not. Never written by hand; the stylesheet reads it to clip the row and show the hand. [ Optional ] data-drag-slider-dragging Written by the engine on the section while a drag runs. Never written by hand; the stylesheet reads it for the closed hand. [ Optional ] data-drag-slider-active Written by the engine on the card the counter reads. Never written by hand; the stylesheet reads it to put that card's number in ink. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same section. An attribute always wins over the defaults written in the code.Attribute Default What it is data-drag-slider-glide 0.32 How long a throw takes to lose most of its speed, in seconds; the row runs on about glide times the release speed. data-drag-slider-settle 0.5 How long the row takes to come back from past an end, or to reach a card brought in by the keyboard, in seconds. data-drag-slider-stretch 0.55 How much the row gives when it is pulled or thrown past an end, 0 to 1. data-drag-slider-threshold 6 How far a press travels sideways, in pixels, before it becomes a drag; a shorter press is a click.
Two files, no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
DragSlider.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-drag-slider-glide 0.32 How long a throw takes to lose most of its speed, in seconds; the row runs on about glide times the release speed. The row stops soon after the hand lets go, a short and firm throw The row coasts far and long, a light and slippery row data-drag-slider-settle 0.5 How long the row takes to come back from past an end, or to reach a card brought in by the keyboard, in seconds. A quick, tight return A slow, soft return data-drag-slider-stretch 0.55 How much the row gives when it is pulled or thrown past an end, 0 to 1. 0 is a wall: the row stops dead at its ends The row stretches further past its ends before the spring takes it back data-drag-slider-threshold 6 How far a press travels sideways, in pixels, before it becomes a drag; a shorter press is a click. A drag starts at the slightest move, and a shaky click may be read as one A press has to travel further before the row moves, and clicks are safer -
Drive it from JavaScript
When the cards change after load, when a button has to bring a card in, or when the page reacts to the card the counter reads.const element = document.querySelector('[data-drag-slider]'); element.addEventListener('dragslider:change', (e) => { console.log(e.detail); }); const slider = DragSlider.mount(element); // the section auto() mounted, or a new one slider.select(4); // throws the row to rest on the fifth card slider.refresh(); // after the cards changed element.addEventListener('dragslider:change', (e) => { console.log(e.detail.index); // the card the counter reads }); // When the slider leaves the page: DragSlider.destroy(element); // gives back the markup as it was
-
Fit it to your page
- Mark the parts.
data-drag-slideron the section, then inside it-viewporton the box that clips the row,-trackon the row itself and-cardon each card, a child of the track. Optional:-currentand-totalon the two numbers of the counter,-baron the fill of the progress rule. A track with no card marked takes its own children as the cards; without-viewport, the parent of the track clips. - The cards are links, and stay links. A press opens its card when it travels less than
thresholdpixels. Past that it is a drag: the viewport takes the pointer, and the click the browser would send on release is swallowed, so a drag never opens a card. A press on a row still gliding stops it where it is and opens nothing. - One setting for every slider of the page, without opening the file:
window.DragSliderSettings = { glide: 0.5 }in a script placed beforedrag-slider.js. Three levels, least specific first:DEFAULTS, then that object, then adata-drag-slider-*attribute on the section. - Inputs. Mouse and finger through Pointer Events. The viewport has
touch-action: pan-y: a vertical swipe on the row scrolls the page, a sideways one moves the row. A trackpad swiped sideways, or a wheel with shift, moves the row exactly and stops at its ends; a vertical wheel is left to the page. The arrow keys move the focus from card to card, Home and End to the first and last, and a card that takes the focus from the keyboard comes into view. - States, written by the engine on the section:
data-drag-slider-stateisreadywhen the row is longer than its viewport andfitwhen it is not (nothing to drag, the page's own cursors), anddata-drag-slider-draggingis there while a drag runs. The hand cursor reads them:grabover the row,grabbingwhile it drags. - The card the counter reads carries
data-drag-slider-active. Each card has a reading place, its left edge on the track's inner margin; the last cards, which the end of the row keeps from getting there, share the room left evenly, so the counter reads 01 at the start, the last number at the end, and never skips one. - Reduced motion: the row follows the hand, stops where it is let go, never passes its ends, and a card brought in by the keyboard jumps into place. The counter's digits do not roll.
- From JavaScript:
DragSlider.mount(el, overrides)returns the instance:element,settings,cards,index()(the card the counter reads),progress()(0 to 1),position()(the row's offset in pixels),speed(),phase(),go(i)(brings card i into view, as the keyboard does),select(i)(throws the row so that its glide comes to rest on card i),refresh()(measures again) anddestroy().destroy()takes off every listener, the watch on sizes, the counter it built, the states and every style it wrote, so the markup is as it was.DragSlider.destroy(el),DragSlider.refresh(el),DragSlider.select(el, i),DragSlider.auto(root)andDragSlider.defaultscomplete it. In React,mountinuseEffectanddestroyin its cleanup. - Event:
dragslider:change, bubbling, each time the counter reads another card,detail{ element, index, card, cards }. It fires once at mount, for the first card. - With the cursor label. The row shows the system hand. On a page that also runs the cursor label component,
data-cursor-label="Drag"on the viewport swaps the hand for an ink pill saying Drag: the pill follows the drag, since the viewport holds the pointer, and its stylesheet hides the hand there with!important. The two are independent; nothing else to wire. - The preview ships whole.
snippet.htmland the last part ofdrag-slider.css, after the linelook, left out of the Webflow Embed, are the preview: the same header, intro, cards, words and foot, in Geist and Geist Mono, loaded by the font tag of the Code tab. Thedrag-slider-*classes are never read by the engine: on a row of your own, keep your classes, add the attributes, and drop that last part of the file and the font tag. The states of the look (the number in ink with its short rule on the card the counter reads, the keyboard ring, the header links) sit just before that line and find their elements by the attributes the preview's markup carries for them,data-drag-slider-number,-card-inkand-link, never by a class: Webflow renames a class pasted twice on one site (AB-116). Drop them too. - On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- The words of the markup are the preview's, and they ship as they are: pasted, the page reads the way the preview did. Put the user's own content in their place whenever they want, in the page's language.
- Mark the parts.
-
Avoid the pitfalls
- Give the cards a width. The row is a flex line that never wraps, and a card with no width of its own shrinks to its words. The preview's cards are sized on the screen height as much as its width,
aspect-ratio: 4 / 5. - The viewport clips, the page does not have to. With the script, the viewport is
overflow-x: clip, which cuts the row without making a scroll container, so a focused card cannot scroll it behind the engine's back. Do not setoverflow: hiddenon it in its place, and do not give the track a transform of your own: the engine writes it every frame. - A handle inside a card. A button or a field inside a card is pressed like the rest of the card: the press becomes a drag past
threshold, and the click after a drag is swallowed. A control that must never drag the row stops thepointerdownfrom reaching the viewport. - **Settings handed to
mount()after the page loaded are dropped.** The script mounts every[data-drag-slider]on DOM ready, andmount()on a mounted section returns the running instance. A page-wide block goes inwindow.DragSliderSettings, whichauto()reads itself; a row whose cards changed is measured again byrefresh(). - Without the script, or before it runs, the row is a plain one that scrolls sideways and snaps card by card: the content is never locked away.
- Give the cards a width. The row is a flex line that never wraps, and a card with no width of its own shrinks to its words. The preview's cards are sized on the screen height as much as its width,
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Drag slider is a scroll animation for Webflow and vanilla JavaScript, with no library. Also called draggable slider with momentum, draggable slider or drag carousel inertia.
A row of cards driven by the hand, with the mouse or a finger. Grabbed, the row follows the pointer; thrown, it runs on with the speed it was given, slows down on its own and comes to rest where that speed runs out. Pulled past either end it gives less and less, like rubber, and a spring brings it back without overshooting. A counter reads the card in front ("03 / 09") on rolling digits and a rule fills as the row goes. A press that does not travel stays a click, so the cards are real links; a drag never opens one, and a press on a row still running stops it where it is. A vertical swipe still scrolls the page, a trackpad or a wheel with shift moves the row, the arrow keys step from card to card, and under reduced motion the row follows the hand and nothing runs on. No library: Pointer Events and one transform per frame, on a clock that sleeps at rest. Pattern type draggable carousel, momentum scroll, flick slider.
Updated
Questions
Mark the section with the custom attribute data-drag-slider, then add data-drag-slider-viewport on the box that clips the row, data-drag-slider-track on the row and data-drag-slider-card on each card. Give the cards a width: the row is a flex line that never wraps, and a card with no width of its own shrinks to its words.
No. It needs no library and no build step: it runs on Pointer Events and one transform per frame, on a clock that sleeps at rest.
Set data-drag-slider-glide, the time a throw takes to lose most of its speed, 0.32 seconds by default. Lower and the row stops soon after the hand lets go; higher and it coasts far and long. data-drag-slider-stretch, 0.55 by default, is how much the row gives when pulled past an end, and 0 makes the ends a wall.
Yes. A press that travels less than threshold pixels, 6 by default, stays a click and opens its card. Past that it is a drag, and the click the browser would send on release is swallowed, so a drag never opens a card.
No. The viewport has touch-action: pan-y, so a vertical swipe on the row scrolls the page and a sideways one moves the row. The arrow keys move the focus from card to card, and under reduced motion the row follows the hand, stops where it is let go and never passes its ends.