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)
<Pagination currentPage={1} totalPages={1} showWhenSinglePage={true} />
showWhenSinglePage={true}
Live Preview
Single (Hidden)
<Pagination currentPage={1} totalPages={1} showWhenSinglePage={false} />
Hidden (1 Page)
Live Preview
Multiple Pages
<Pagination currentPage={3} totalPages={5} />
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>
  );
}