Hooks
useDragScroll
Drag-to-scroll with momentum and fading edges.
Grab the strip, pull — it follows the cursor and coasts to a stop after you let go. Like panning a graph, but for an ordinary scrollable container.
Example
Зажмите и потяните. Отпустите на движении — лента доедет по инерции. Края растворяются там, где спрятан контент.
import { useDragScroll } from "@toimetdev/pathlogs-hooks";
function Board() {
const ref = useDragScroll<HTMLDivElement>({ axis: "x", keyboard: true });
return (
<div ref={ref} className="flex gap-3 overflow-x-auto">
{columns.map((c) => <Column key={c.id} {...c} />)}
</div>
);
}The hook returns a ref callback — hanging it on the container is enough. The scroll classes (overflow-x-auto and friends) are yours to set: the hook is responsible for behaviour, not layout.
Options
| Prop | Type | Default | Description |
|---|---|---|---|
axis | "x" | "y" | "both" | "x" | The scrolling axis. Keys across the axis are not intercepted — the page is waiting for those. |
momentum | boolean | true | Inertia after a fling. Switched off automatically under prefers-reduced-motion. |
keyboard | boolean | false | The strip joins the tab order and listens for arrows, Page and Home/End — but only while focus is on the strip itself, not on an element inside it. |
enabled | boolean | true | Temporarily disable dragging without removing the hook. |
axis"x" | "y" | "both"defaults to: "x"
The scrolling axis. Keys across the axis are not intercepted — the page is waiting for those.
momentumbooleandefaults to: true
Inertia after a fling. Switched off automatically under prefers-reduced-motion.
keyboardbooleandefaults to: false
The strip joins the tab order and listens for arrows, Page and Home/End — but only while focus is on the strip itself, not on an element inside it.
enabledbooleandefaults to: true
Temporarily disable dragging without removing the hook.
Both axes
With axis: "both" the canvas drags in any direction — this is how the Gantt chart is panned:
const ref = useDragScroll<HTMLDivElement>({ axis: "both", momentum: false });
<div ref={ref} className="h-56 overflow-auto">
<div className="w-[48rem]">…</div>
</div>How it works
There is a lot here that is not obvious, and almost every decision is an answer to a specific breakage:
- Clicks do not break. Dragging engages only past a 5px movement threshold, and a click «caught» by it is suppressed during the capture phase. Without this, a shaky hand while clicking a card would open the card instead of scrolling — or the other way round.
- Native drag&drop wins. On
dragstartthe drag is cancelled and the strip starts auto-scrolling near the edge. Otherwise there would be no way to carry a card to a column off-screen: during a native drag the pointer belongs to the browser. - Touch is left alone. Finger scrolling is already native there, and intercepting it would only make it worse.
- Cursor and edges follow reality. The «grab» cursor and the fading edges appear only when the strip actually has somewhere to go.
- Inertia dies against the edge. Hit the end and velocity is zeroed, so the strip does not judder at the boundary. A long frame (the tab was in the background) does not teleport the strip: the step is capped at 50ms.
Styles
The visual half lives in @toimetdev/pathlogs-tokens/styles/scroll.css and hooks onto the data attributes the hook sets:
<div data-pl-drag-scroll="true" <!-- there is room to scroll: «grab» cursor -->
data-pl-scroll-edge="both" <!-- content is hidden left and right -->
data-pl-scroll-edge-y="end" <!-- and below -->
data-pl-scroll-keys="true"> <!-- listening for keys -->The maths, separately
All the arithmetic is lifted out of the hook and knows nothing about the DOM — which is why it is covered by tests rather than checked by hand:
import {
isDragIntent, // movement passed the threshold — a drag, not a click
flingVelocity, // fling speed from the pointer track
decayVelocity, // inertia decay per frame
edgeScrollSpeed, // auto-scroll speed near the edge during drag&drop
hiddenEdges, // which edge «continues» off-screen
keyboardScroll, // what a given key press should do
} from "@toimetdev/pathlogs-hooks";The same module holds attachDragScroll(el, getOptions) — binding to an element without React, if you need the behaviour outside the React tree.