Infinite Circular Scroll
Vertical list of cards with infinite scroll. Scroll past the last to wrap to the first.
Component
"use client";
import React, { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
interface InfiniteCircularScrollProps {
children: React.ReactNode;
className?: string;
itemClassName?: string;
itemHeight?: number;
}
export function InfiniteCircularScroll({
children,
className,
itemClassName,
itemHeight = 64,
}: InfiniteCircularScrollProps) {
const containerRef = useRef<HTMLDivElement>(null);
const items = React.Children.toArray(children);
const count = Math.max(1, items.length);
const totalHeight = count * itemHeight;
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const handleScroll = () => {
const { scrollTop } = container;
if (scrollTop < totalHeight * 0.5) {
container.scrollTop = scrollTop + totalHeight;
} else if (scrollTop > totalHeight * 2.5) {
container.scrollTop = scrollTop - totalHeight;
}
};
const handleWheel = (e: WheelEvent) => {
e.stopPropagation();
};
container.addEventListener("scroll", handleScroll);
container.addEventListener("wheel", handleWheel, {
passive: false,
});
container.scrollTop = totalHeight;
return () => {
container.removeEventListener("scroll", handleScroll);
container.removeEventListener("wheel", handleWheel);
};
}, [totalHeight]);
return (
<div
ref={containerRef}
data-infinite-scroll={true}
className={cn(
"overflow-y-auto overflow-x-hidden [&::-webkit-scrollbar]:hidden",
className,
)}
style={{
height: itemHeight * 3,
msOverflowStyle: "none",
overscrollBehavior: "contain",
scrollbarWidth: "none",
}}
>
<div className="flex flex-col">
{[1, 2, 3].map((copy) => (
<React.Fragment key={copy}>
{items.map((child, i) => {
const key =
React.isValidElement(child) &&
child.key != null
? String(child.key)
: `item-${copy}-${i}`;
return (
<div
key={key}
className={cn(
"flex shrink-0 items-center",
itemClassName,
)}
style={{ height: itemHeight }}
>
{child}
</div>
);
})}
</React.Fragment>
))}
</div>
</div>
);
}Installation
pnpm dlx shadcn@latest add "https://pulkitxm.com/components/infinite-circular-scroll.json"1. Copy the component file
"use client";
import React, { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
interface InfiniteCircularScrollProps {
children: React.ReactNode;
className?: string;
itemClassName?: string;
itemHeight?: number;
}
export function InfiniteCircularScroll({
children,
className,
itemClassName,
itemHeight = 64,
}: InfiniteCircularScrollProps) {
const containerRef = useRef<HTMLDivElement>(null);
const items = React.Children.toArray(children);
const count = Math.max(1, items.length);
const totalHeight = count * itemHeight;
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const handleScroll = () => {
const { scrollTop } = container;
if (scrollTop < totalHeight * 0.5) {
container.scrollTop = scrollTop + totalHeight;
} else if (scrollTop > totalHeight * 2.5) {
container.scrollTop = scrollTop - totalHeight;
}
};
const handleWheel = (e: WheelEvent) => {
e.stopPropagation();
};
container.addEventListener("scroll", handleScroll);
container.addEventListener("wheel", handleWheel, {
passive: false,
});
container.scrollTop = totalHeight;
return () => {
container.removeEventListener("scroll", handleScroll);
container.removeEventListener("wheel", handleWheel);
};
}, [totalHeight]);
return (
<div
ref={containerRef}
data-infinite-scroll={true}
className={cn(
"overflow-y-auto overflow-x-hidden [&::-webkit-scrollbar]:hidden",
className,
)}
style={{
height: itemHeight * 3,
msOverflowStyle: "none",
overscrollBehavior: "contain",
scrollbarWidth: "none",
}}
>
<div className="flex flex-col">
{[1, 2, 3].map((copy) => (
<React.Fragment key={copy}>
{items.map((child, i) => {
const key =
React.isValidElement(child) &&
child.key != null
? String(child.key)
: `item-${copy}-${i}`;
return (
<div
key={key}
className={cn(
"flex shrink-0 items-center",
itemClassName,
)}
style={{ height: itemHeight }}
>
{child}
</div>
);
})}
</React.Fragment>
))}
</div>
</div>
);
}2. Import and use
import { InfiniteCircularScroll } from "@/components/infinite-circular-scroll";
<InfiniteCircularScroll itemHeight={56}>
{items.map((item) => (
<Card key={item.id} {...item} />
))}
</InfiniteCircularScroll>;Usage
Import
Add the import.
import { InfiniteCircularScroll } from "@/components/infinite-circular-scroll";Use
Pass cards as children. Scroll infinitely.
<InfiniteCircularScroll itemHeight={56}>
{items.map((item) => (
<Card key={item.id} {...item} />
))}
</InfiniteCircularScroll>;Guidelines
- Pass cards as children. They are arranged in a vertical list.
- Scrollbar is hidden. Scroll wraps infinitely.
- itemHeight must match your card height for correct alignment.
Props
All props are optional unless marked required. Use these to customize every aspect of the component.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | — | Cards or items to display in the list. |
| className | string | — | Additional CSS classes for the container. |
| itemClassName | string | — | Additional CSS classes for each item wrapper. |
| itemHeight | number | 64 | Height of each item in pixels. |
Examples
Basic
Numbered cards 1–10. Scroll infinitely.
import { InfiniteCircularScroll } from "@/components/infinite-circular-scroll";
<InfiniteCircularScroll
itemHeight={56}
className="rounded-lg border border-border"
>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => (
<div
key={i}
className="flex items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2"
>
<span className="font-mono font-bold">{i}</span>
<span className="text-muted-foreground text-sm">
Card {i}
</span>
</div>
))}
</InfiniteCircularScroll>;