This document outlines the steps to create Pagination
component styled withTailwind CSS and using some npm dependency libraries.
Node.js
and npm
installed on your machine.Tailwind CSS
installed in your project.CVA(class-variance-authority)
is a utility for managing CSS class names based on various conditions.clsx
is a tiny utility for constructing className strings conditionally.1// pagination.component.tsx
2import clsx from "clsx";
3import { ChevronLeftIcon, ChevronRightIcon, EllipsisIcon } from "lucide-react";
4import { useMemo } from "react";
5
6export interface PaginationProps {
7 total: number;
8 current: number;
9 siblings?: number;
10 showControls?: boolean;
11 variant?: "solid" | "separated" | "outline";
12 className?: string;
13 cellClassName?: string;
14 onChange: (val: number) => void;
15}
16
17const Pagination: React.FC<PaginationProps> = ({
18 total,
19 current,
20 siblings = 2,
21 variant = "solid",
22 showControls,
23 className,
24 cellClassName,
25 onChange,
26}) => {
27 const cells = useMemo(() => {
28 const maxSequentialPages = siblings * 2 + 1;
29
30 const paginationList: Array<string | number> = [];
31
32 if (showControls) paginationList.push("<");
33
34 if (total < maxSequentialPages) {
35 Array(total).forEach((_, index) => paginationList.push(index + 1));
36 } else {
37 let start = Math.max(1, current - siblings);
38 let end = Math.min(total, current + siblings);
39
40 if (current <= siblings + 1) end = maxSequentialPages;
41
42 if (current >= total - siblings) start = total - maxSequentialPages + 1;
43
44 if (start > 1) paginationList.push(1);
45 if (start > 2) paginationList.push("...");
46
47 for (let i = start; i <= end; i++) paginationList.push(i);
48
49 if (end < total - 1) paginationList.push("...");
50 if (end < total) paginationList.push(total);
51 }
52
53 if (showControls) paginationList.push(">");
54
55 return paginationList;
56 }, [siblings, showControls, current, total]);
57
58 const onPrev = () => {
59 if (current <= 1) return;
60 onChange(current - 1);
61 };
62
63 const onNext = () => {
64 if (current >= total) return;
65 onChange(current + 1);
66 };
67
68 const handleClick = (item: string | number) => {
69 if (typeof item === "number") {
70 onChange(item);
71 } else if (item === "<") {
72 onPrev();
73 } else if (item === ">") {
74 onNext();
75 }
76 };
77
78 return (
79 <div
80 className={clsx(
81 "flex h-10 w-fit",
82 {
83 "rounded-lg bg-neutral-100 dark:bg-neutral-700": variant === "solid",
84 "gap-2": variant === "separated",
85 },
86 className,
87 )}
88 >
89 {cells.map((item, index) => (
90 <div
91 key={index}
92 className={clsx(
93 "flex min-w-10 items-center justify-center px-1",
94 { "cursor-pointer": item !== "..." },
95 {
96 "border-l-[1px] border-neutral-200 dark:border-neutral-600":
97 variant === "solid" &&
98 index !== 0 &&
99 item !== current &&
100 item !== current + 1,
101 },
102 {
103 "rounded-lg bg-blue-500 text-white": item === current,
104 "rounded-lg bg-neutral-100 text-black dark:bg-neutral-700 dark:text-neutral-100":
105 variant === "separated" && item !== current,
106 },
107 cellClassName,
108 )}
109 onClick={() => handleClick(item)}
110 >
111 {typeof item === "number" ? (
112 <span className="select-none font-medium">{item}</span>
113 ) : item === "<" ? (
114 <ChevronLeftIcon
115 className={clsx("h-4 w-4", {
116 "text-neutral-300 dark:text-neutral-600": current <= 1,
117 })}
118 />
119 ) : item === ">" ? (
120 <ChevronRightIcon
121 className={clsx("h-4 w-4", {
122 "text-neutral-300 dark:text-neutral-600": current >= total,
123 })}
124 />
125 ) : (
126 <EllipsisIcon className="h-3 w-3" />
127 )}
128 </div>
129 ))}
130 </div>
131 );
132};
133
134export default Pagination;
135