Retour au catalogue
Pagination Infinite
Infinite scroll avec sentinelle et spinner de chargement.
paginationsimple Both Responsive a11y
minimaluniversalstacked
Theme
"use client";
import { useRef, useEffect, useState, useCallback } from "react";
import { Loader2 } from "lucide-react";
interface InfiniteItem {
id: number;
title: string;
excerpt: string;
}
interface PaginationInfiniteProps {
maxItems?: number;
batchSize?: number;
}
function generateItems(start: number, count: number): InfiniteItem[] {
return Array.from({ length: count }, (_, i) => ({
id: start + i + 1,
title: `Article ${start + i + 1}`,
excerpt: "Apercu du contenu de cet article avec un court resume descriptif.",
}));
}
export default function PaginationInfinite({
maxItems = 24,
batchSize = 6,
}: PaginationInfiniteProps) {
const [items, setItems] = useState<InfiniteItem[]>(() => generateItems(0, batchSize));
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const sentinelRef = useRef<HTMLDivElement>(null);
const loadMore = useCallback(() => {
if (loading || !hasMore) return;
setLoading(true);
setTimeout(() => {
setItems((prev) => {
const newItems = generateItems(prev.length, batchSize);
const merged = [...prev, ...newItems];
if (merged.length >= maxItems) setHasMore(false);
return merged;
});
setLoading(false);
}, 600);
}, [loading, hasMore, batchSize, maxItems]);
useEffect(() => {
const el = sentinelRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) loadMore();
},
{ threshold: 0.1 }
);
observer.observe(el);
return () => observer.disconnect();
}, [loadMore]);
return (
<div
className="py-8 px-6"
style={{ background: "var(--color-background)" }}
>
<div className="mx-auto max-w-2xl flex flex-col gap-3">
{items.map((item) => (
<div
key={item.id}
className="p-4 rounded-xl transition-opacity duration-300"
style={{
background: "var(--color-background-card)",
border: "1px solid var(--color-border)",
animation: "infinite-fade-in 0.3s ease-out",
}}
>
<h3
className="text-sm font-semibold"
style={{ color: "var(--color-foreground)" }}
>
{item.title}
</h3>
<p
className="mt-1 text-sm"
style={{ color: "var(--color-foreground-muted)" }}
>
{item.excerpt}
</p>
</div>
))}
<div ref={sentinelRef} className="flex items-center justify-center py-8">
{loading && (
<div className="flex flex-col items-center gap-2">
<Loader2
size={20}
className="animate-spin"
style={{ color: "var(--color-accent)" }}
/>
<span
className="text-xs"
style={{ color: "var(--color-foreground-light)" }}
>
Chargement...
</span>
</div>
)}
{!hasMore && (
<span
className="text-sm"
style={{ color: "var(--color-foreground-light)" }}
>
Tous les elements ont ete charges
</span>
)}
</div>
</div>
<style>{`
@keyframes infinite-fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
</div>
);
}
Autres variantes pagination
Pagination Cursor Based
medium · both
minimalcorporate
Pagination Infinite Scroll
medium · both
minimalcorporate
Pagination Infinite Sentinel
medium · both
minimalcorporate
Pagination Load More Cards
medium · both
minimalelegant
Pagination Load More
simple · both
minimalplayful
Pagination Numbered
simple · both
minimalcorporate