webdevpuneet.com

Drag Scroll Row — Free HTML CSS JS Momentum Carousel Snippet

Drag Scroll Row · Carousels · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="ds-wrap">
  <h2 class="ds-title">Continue watching</h2>
  <div class="ds-row" id="dsRow" tabindex="0" aria-label="Scrollable gallery, drag to browse">
    <div class="ds-track" id="dsTrack"></div>
  </div>
  <div class="ds-hint">Drag, flick, or use the arrow keys</div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Pointer Events drag
One path for mouse and touch dragging.
Pointer capture
Keeps tracking even off the element.
Momentum glide
Velocity decays for a natural flick.
Edge clamping
Never scrolls past the first or last tile.
Catch mid-flight
A new press cancels the running glide.
Edge fade mask
Tiles dissolve at the row's ends.
Grab cursors
grab/grabbing signal draggability.
Keyboard nudge
Arrow keys move by one tile width.

About this UI Snippet

Drag Scroll Row — Flick-to-Scroll Gallery With Momentum

Screenshot of the Drag Scroll Row snippet rendered live

The drag scroll row is the Netflix-style horizontal shelf you grab and fling: press anywhere on the row and drag to scroll, flick and release to let it glide on momentum, and it eases to a stop at the edges. This snippet builds it with plain HTML, CSS, and vanilla JavaScript using Pointer Events, so it works identically for mouse and touch.

Dragging with Pointer Events and capture

On pointerdown, the row records the start cursor x and the current track position, then calls setPointerCapture so it keeps receiving move events even if the pointer leaves the element mid-drag. As you move, the track's translateX is set to the start position plus the cursor delta, clamped to the scrollable range. Using Pointer Events (rather than separate mouse and touch handlers) means one code path drives both desktop dragging and mobile swiping.

Momentum on release

The flick feel comes from velocity tracking. During the drag, each move records the per-frame cursor delta as vel. On release, a glide() loop keeps moving the track by vel while multiplying vel by 0.93 every frame, so it decays exponentially and coasts to a stop — just like flicking a physical shelf. The loop ends once velocity drops below a small threshold. Cancelling any running glide on the next pointerdown lets you catch a moving row mid-flight.

Edge clamping

The track can only scroll between 0 and a negative minimum equal to rowWidth - trackWidth (computed in bounds()). Every position update runs through clamp(), so neither dragging nor momentum can push the content past its ends — it stops cleanly at the first and last tiles. A resize listener recomputes the bounds and re-clamps, so the row stays valid when the viewport changes.

Edge fade and grab cursors

A horizontal mask-image gradient fades the row's left and right edges so tiles dissolve in and out rather than hard-cutting at the container border. The cursor switches between grab and grabbing via a .drag class so it's obvious the row is draggable. Tiles set user-select: none so dragging never accidentally selects text or images.

Keyboard accessible

The row is focusable (tabindex="0") with a visible focus ring, and Arrow Left/Right nudge it by one tile width — so it's fully operable without a pointer, which a drag-only carousel would fail. An aria-label describes it as a draggable gallery.

Image-free, data-driven tiles

Tiles are gradient "posters" with a title and caption, generated from a palette so the demo is dependency-free. Swap each poster's --g background for a real image to make it an actual media shelf; the drag, momentum, clamping, and keyboard logic are independent of the content.

Customizing it

Tune the 0.93 decay for longer or shorter glides, change the velocity cutoff, adjust the arrow-key nudge distance, resize the tiles, or drop in real images. Add more shelves by repeating the structure. Pair it with a recently viewed carousel or an Instagram gallery for a complete media UI.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA horizontal shelf of poster tiles renders.
  2. 2
    Drag the rowPress and move to scroll the tiles left or right.
  3. 3
    Flick and releaseThe row glides on momentum and eases to a stop.
  4. 4
    Reach an edgeScrolling clamps cleanly at the first and last tiles.
  5. 5
    Use arrow keysFocus the row and press Left or Right to nudge it.
  6. 6
    Swap in imagesSet each poster's --g to a real image.

Real-world uses

Common Use Cases

Streaming media shelves
Build a recently-viewed shelf like the recently viewed carousel, where users grab anywhere on the row and flick it to glide on momentum.
Product rails
Browse items beside a product card grid, with edge clamping so the row never scrolls past the first or last tile.
Image galleries
Pair with an Instagram gallery to give a horizontal browsing option, using pointer capture so tracking continues off the element.
Category browsing
Flick through sections alongside a chip filter, with fade masks at the edges hinting that more content is available.
Dashboard widget rows
Scroll a row of metric card grid widgets, and study velocity decay as a reference for natural-feeling flick scrolling.
Related: Floating Share Dock
See the Floating Share Dock for a related navigation pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Pointer Events unify mouse, touch, and pen into one API, so a single set of pointerdown/move/up handlers drives both desktop dragging and mobile swiping. setPointerCapture also keeps the row receiving move events even if the pointer drifts off the element mid-drag, which separate mouse/touch code makes awkward.

During the drag, each move records the per-frame cursor delta as velocity. On release, a glide loop keeps translating the track by that velocity while multiplying it by 0.93 each frame, so it decays exponentially and coasts to a stop — like flicking a physical shelf. The loop ends when velocity falls below a small threshold.

The scrollable range is 0 to a negative minimum equal to the row width minus the track width. Every position update — from dragging or from momentum — passes through a clamp to that range, so the content can never be pushed past its first or last tile. A resize listener recomputes the bounds and re-clamps when the viewport changes.

Yes. The row is focusable with a visible focus ring, and Arrow Left/Right nudge it by one tile width, so keyboard users can browse it. It also carries an aria-label describing it as a draggable gallery. A drag-only carousel would exclude keyboard users, so the arrow support matters for accessibility.

Render the tiles from data and keep position, velocity, and dragging in refs (not state) so dragging doesn't re-render. Attach the pointer and keydown handlers in a mount effect with cleanup, using refs for the row and track. The momentum loop lives in the same effect. The CSS, including the edge mask, ports directly; in Tailwind use overflow-hidden with an arbitrary mask-image.