Overview
useScroller manages horizontal scrolling state and interactions for a scrollable container. Provides directional scroll helpers, edge availability flags, and optional drag-to-scroll handlers.
Installation
Import
import { useScroller } from "@kuzenbo/hooks";
Usage
import { useScroller } from "@kuzenbo/hooks";
export function HorizontalScroller() {
const { ref, canScrollStart, canScrollEnd, scrollStart, scrollEnd } = useScroller();
return (
<div className="relative">
<div className="flex gap-2 mb-2">
<button
onClick={scrollStart}
disabled={!canScrollStart}
className="px-3 py-1 bg-primary text-primary-foreground rounded disabled:opacity-50"
>
← Scroll Left
</button>
<button
onClick={scrollEnd}
disabled={!canScrollEnd}
className="px-3 py-1 bg-primary text-primary-foreground rounded disabled:opacity-50"
>
Scroll Right →
</button>
</div>
<div
ref={ref}
className="flex gap-4 overflow-x-auto scrollbar-hide"
>
{Array.from({ length: 20 }, (_, i) => (
<div
key={i}
className="flex-shrink-0 w-48 h-32 bg-muted rounded-lg flex items-center justify-center"
>
Item {i + 1}
</div>
))}
</div>
</div>
);
}
import { useScroller } from "@kuzenbo/hooks";
export function DraggableScroller() {
const { ref, dragHandlers, isDragging } = useScroller({
draggable: true,
scrollAmount: 300,
});
return (
<div>
<p className="mb-2 text-sm text-muted-foreground">
{isDragging ? "Dragging..." : "Click and drag to scroll"}
</p>
<div
ref={ref}
{...dragHandlers}
className="flex gap-4 overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
>
{Array.from({ length: 15 }, (_, i) => (
<div
key={i}
className="flex-shrink-0 w-64 h-40 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center text-white font-semibold"
>
Card {i + 1}
</div>
))}
</div>
</div>
);
}
import { useScroller } from "@kuzenbo/hooks";
import { useState } from "react";
export function ScrollerWithCallback() {
const [scrollState, setScrollState] = useState({ canScrollStart: false, canScrollEnd: true });
const { ref, scrollStart, scrollEnd } = useScroller({
scrollAmount: 250,
onScrollStateChange: (state) => setScrollState(state),
});
return (
<div>
<div className="mb-4 p-4 bg-muted rounded-lg">
<p className="text-sm">Scroll State:</p>
<p className="text-xs text-muted-foreground">
Can scroll left: {scrollState.canScrollStart ? "Yes" : "No"}
</p>
<p className="text-xs text-muted-foreground">
Can scroll right: {scrollState.canScrollEnd ? "Yes" : "No"}
</p>
</div>
<div className="flex gap-2 mb-2">
<button onClick={scrollStart} className="px-3 py-1 bg-muted rounded">←</button>
<button onClick={scrollEnd} className="px-3 py-1 bg-muted rounded">→</button>
</div>
<div
ref={ref}
className="flex gap-4 overflow-x-auto scrollbar-hide"
>
{Array.from({ length: 12 }, (_, i) => (
<div key={i} className="flex-shrink-0 w-56 h-36 bg-muted rounded-lg" />
))}
</div>
</div>
);
}
RTL Support
import { useScroller } from "@kuzenbo/hooks";
export function RTLScroller() {
const { ref, canScrollStart, canScrollEnd, scrollStart, scrollEnd } = useScroller();
return (
<div dir="rtl">
<div className="flex gap-2 mb-2">
<button
onClick={scrollStart}
disabled={!canScrollStart}
className="px-3 py-1 bg-primary text-primary-foreground rounded"
>
→ Scroll Right
</button>
<button
onClick={scrollEnd}
disabled={!canScrollEnd}
className="px-3 py-1 bg-primary text-primary-foreground rounded"
>
← Scroll Left
</button>
</div>
<div
ref={ref}
className="flex gap-4 overflow-x-auto scrollbar-hide"
>
{Array.from({ length: 15 }, (_, i) => (
<div key={i} className="flex-shrink-0 w-48 h-32 bg-muted rounded-lg" />
))}
</div>
</div>
);
}
API Reference
function useScroller<T extends HTMLElement = HTMLDivElement>(
options?: UseScrollerOptions
): UseScrollerReturnValue<T>
Scroller behavior configurationPixel distance used by scrollStart and scrollEnd
Enables mouse drag scrolling when true
options.onScrollStateChange
(state: UseScrollerScrollState) => void
Called whenever canScrollStart or canScrollEnd changes
Ref callback to attach to the scrollable container element
Whether content can be scrolled towards the start (left in LTR, right in RTL)
Whether content can be scrolled towards the end (right in LTR, left in RTL)
Scrolls towards the start direction
Scrolls towards the end direction
true if the user is currently dragging the content
Props to spread on the scrollable container for drag functionalityonMouseDown
(e: React.MouseEvent) => void
Mouse down handler
onMouseMove
(e: React.MouseEvent) => void
Mouse move handler
Type Definitions
interface UseScrollerOptions {
scrollAmount?: number;
draggable?: boolean;
onScrollStateChange?: (state: UseScrollerScrollState) => void;
}
interface UseScrollerScrollState {
canScrollStart: boolean;
canScrollEnd: boolean;
}
interface UseScrollerReturnValue<T extends HTMLElement = HTMLDivElement> {
ref: RefCallback<T | null>;
canScrollStart: boolean;
canScrollEnd: boolean;
scrollStart: () => void;
scrollEnd: () => void;
isDragging: boolean;
dragHandlers: {
onMouseDown: (e: React.MouseEvent) => void;
onMouseMove: (e: React.MouseEvent) => void;
onMouseUp: () => void;
onMouseLeave: () => void;
};
}
Caveats
- Automatically detects RTL direction from computed styles
- Uses
scrollBy with smooth behavior for scroll functions
- Drag-to-scroll suppresses click events when dragging distance exceeds 5px
- Sets
cursor: grabbing and user-select: none during drag
- Updates scroll state on resize using
useResizeObserver
SSR and RSC Notes
- Use this hook in Client Components only
- Do not call it from React Server Components