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 console.log(siblings, showControls, current, total);
29 console.log(typeof siblings);
30
31 const maxSequentialPages = siblings * 2 + 1;
32
33 const paginationList: Array<string | number> = [];
34
35 if (showControls) paginationList.push("<");
36
37 if (total < maxSequentialPages) {
38 Array(total).forEach((_, index) => paginationList.push(index + 1));
39 } else {
40 let start = Math.max(1, current - siblings);
41 let end = Math.min(total, current + siblings);
42
43 if (current <= siblings + 1) end = maxSequentialPages;
44
45 if (current >= total - siblings) start = total - maxSequentialPages + 1;
46
47 if (start > 1) paginationList.push(1);
48 if (start > 2) paginationList.push("...");
49
50 for (let i = start; i <= end; i++) paginationList.push(i);
51
52 if (end < total - 1) paginationList.push("...");
53 if (end < total) paginationList.push(total);
54 }
55
56 if (showControls) paginationList.push(">");
57 console.log(paginationList);
58
59 return paginationList;
60 }, [siblings, showControls, current, total]);
61
62 const onPrev = () => {
63 if (current <= 1) return;
64 onChange(current - 1);
65 };
66
67 const onNext = () => {
68 if (current >= total) return;
69 onChange(current + 1);
70 };
71
72 const handleClick = (item: string | number) => {
73 if (typeof item === "number") {
74 onChange(item);
75 } else if (item === "<") {
76 onPrev();
77 } else if (item === ">") {
78 onNext();
79 }
80 };
81
82 return (
83 <div
84 className={clsx(
85 "flex h-10 w-fit",
86 {
87 "rounded-lg bg-neutral-100 dark:bg-neutral-700": variant === "solid",
88 "gap-2": variant === "separated",
89 },
90 className,
91 )}
92 >
93 {cells.map((item, index) => (
94 <div
95 key={index}
96 className={clsx(
97 "flex min-w-10 items-center justify-center px-1",
98 { "cursor-pointer": item !== "..." },
99 {
100 "border-l-[1px] border-neutral-200 dark:border-neutral-600":
101 variant === "solid" &&
102 index !== 0 &&
103 item !== current &&
104 item !== current + 1,
105 },
106 {
107 "rounded-lg bg-blue-500 text-white": item === current,
108 "rounded-lg bg-neutral-100 text-black dark:bg-neutral-700 dark:text-neutral-100":
109 variant === "separated" && item !== current,
110 },
111 cellClassName,
112 )}
113 onClick={() => handleClick(item)}
114 >
115 {typeof item === "number" ? (
116 <span className="select-none font-medium">{item}</span>
117 ) : item === "<" ? (
118 <ChevronLeftIcon
119 className={clsx("h-4 w-4", {
120 "text-neutral-300 dark:text-neutral-600": current <= 1,
121 })}
122 />
123 ) : item === ">" ? (
124 <ChevronRightIcon
125 className={clsx("h-4 w-4", {
126 "text-neutral-300 dark:text-neutral-600": current >= total,
127 })}
128 />
129 ) : (
130 <EllipsisIcon className="h-3 w-3" />
131 )}
132 </div>
133 ))}
134 </div>
135 );
136};
137
138export default Pagination;
139