Pagination
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
‹
1
2
3
4
5
›
총 47개 · 페이지 3 / 10
Pagination
이전/다음 + 페이지 번호 버튼. showWhenSinglePage로 1페이지만 있을 때 표시 여부를 정합니다 (기본: 항상 표시).
Pagination States
페이지 수와 설정에 따른 다양한 페이징 UI 예시입니다.
Single (Always Show)
showWhenSinglePage={true}
Live Preview
Single (Hidden)
Hidden (1 Page)
Live Preview
Multiple Pages
Multi-page layout
Live Preview
Implementation
제작 코드
이 컴포넌트가 실제로 어떻게 구현되어 있는지 — 본체 .tsx 파일을 그대로 보여줍니다. variant 매핑·접근성 처리·forwardRef 패턴 등 디테일을 그대로 확인할 수 있어요.
Paginationtypescript
/**
* Pagination Component
*
* Professional pagination with smart ellipsis logic and minimal design.
* Handles large page counts gracefully and provides smooth interactions.
*/
"use client";
import React from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@/components/Icon/General";
import { cn } from "@/lib/cn";
export interface PaginationProps {
/** Current active page (1-based) */
currentPage: number;
/** Total number of pages */
totalPages: number;
/** Callback when page is changed */
onPageChange: (page: number) => void;
/** Whether to show UI when only 1 page exists (default: true) */
showWhenSinglePage?: boolean;
/** Extra container className */
className?: string;
/** Max buttons to show before using ellipses (default: 7) */
maxVisible?: number;
}
export function Pagination({
currentPage,
totalPages,
onPageChange,
showWhenSinglePage = true,
className = "",
maxVisible = 7,
}: PaginationProps) {
const current = Math.min(Math.max(1, currentPage), Math.max(1, totalPages));
const total = Math.max(1, totalPages);
if (!showWhenSinglePage && total <= 1) return null;
// Logic to calculate which page numbers to show
const getPageNumbers = () => {
const pages: (number | string)[] = [];
if (total <= maxVisible) {
for (let i = 1; i <= total; i++) pages.push(i);
} else {
// Always show first page
pages.push(1);
const start = Math.max(2, current - 2);
const end = Math.min(total - 1, current + 2);
if (start > 2) pages.push("...");
for (let i = start; i <= end; i++) {
pages.push(i);
}
if (end < total - 1) pages.push("...");
// Always show last page
pages.push(total);
}
return pages;
};
const pageNumbers = getPageNumbers();
const navButton =
"flex size-9 cursor-pointer items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-700 transition-colors " +
"hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 " +
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-slate-200 disabled:hover:bg-white";
return (
<nav
className={cn("flex items-center justify-center gap-1.5 select-none", className)}
aria-label="Pagination Navigation"
>
{/* Previous Button */}
<button
type="button"
onClick={() => onPageChange(current - 1)}
disabled={current <= 1}
className={navButton}
aria-label="Go to previous page"
>
<ChevronLeftIcon className="size-4" />
</button>
{/* Page Numbers */}
<div className="flex items-center gap-1.5">
{pageNumbers.map((p, idx) => {
if (p === "...") {
return (
<span
key={`dots-${idx}`}
className="flex size-9 items-center justify-center text-sm font-medium text-slate-400"
>
•••
</span>
);
}
const isActive = p === current;
return (
<button
key={p}
type="button"
onClick={() => onPageChange(p as number)}
aria-current={isActive ? "page" : undefined}
className={cn(
"flex size-9 cursor-pointer items-center justify-center rounded-lg text-sm transition-colors",
isActive
? "bg-slate-900 font-bold text-white shadow-sm hover:bg-slate-800"
: "border border-slate-200 bg-white font-semibold text-slate-700 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900",
)}
>
{p}
</button>
);
})}
</div>
{/* Next Button */}
<button
type="button"
onClick={() => onPageChange(current + 1)}
disabled={current >= total}
className={navButton}
aria-label="Go to next page"
>
<ChevronRightIcon className="size-4" />
</button>
</nav>
);
}