ページネーション - Vibe Coding Showcase

ページネーション

大量のコンテンツを複数のページに分割して表示するナビゲーションコンポーネント

デザインプレビュー

基本的なページネーション

多数のページ(省略記号あり)

サイズバリエーション

シンプルなページネーション

1 / 10

ページネーションの特徴

スマートな省略表示

多数のページでも見やすい省略記号による表示調整

カスタマイズ可能

表示範囲、ボタンの有無、サイズなど柔軟な設定

アクセシブル

適切なARIA属性とキーボードナビゲーション対応

AI活用ガイド

無限スクロール統合

プロンプト例:

ページネーションと無限スクロールを組み合わせたハイブリッドナビゲーションを実装してください。スクロール位置の記憶、プログレス表示、パフォーマンス最適化、SEO対応を含めてください。

インテリジェントページング

プロンプト例:

ユーザーの行動を学習して最適なページサイズを自動調整するページネーションシステムを作成してください。読み込み速度、画面サイズ、ユーザーの閲覧パターンを考慮してください。

高度なナビゲーション

プロンプト例:

キーボードショートカット、音声コマンド、ジェスチャー操作に対応した次世代ページネーションを実装してください。プレビュー機能、ブックマーク、履歴管理も含めてください。

実装コード

ファイルサイズ: 9.1KB TypeScript

コンポーネント実装

import React, { useMemo } from 'react';
import type { BasePaginationProps } from '../types';

interface PaginationProps extends BasePaginationProps {
  className?: string;
}

export const Pagination: React.FC<PaginationProps> = ({
  currentPage,
  totalPages,
  onPageChange,
  size = 'md',
  showFirstLast = true,
  showPrevNext = true,
  siblings = 1,
  boundaries = 1,
  className = '',
}) => {
  // ページ番号の配列を生成
  const pageNumbers = useMemo(() => {
    const pages: (number | string)[] = [];
    
    // 総ページ数が少ない場合は全て表示
    if (totalPages <= 7) {
      return Array.from({ length: totalPages }, (_, i) => i + 1);
    }

    // 境界ページ
    const leftBoundary = boundaries;
    const rightBoundary = totalPages - boundaries + 1;

    // 現在のページの周辺
    const leftSibling = Math.max(currentPage - siblings, leftBoundary + 2);
    const rightSibling = Math.min(currentPage + siblings, rightBoundary - 2);

    // 左側の省略記号を表示するか
    const showLeftEllipsis = leftSibling > leftBoundary + 2;
    // 右側の省略記号を表示するか
    const showRightEllipsis = rightSibling < rightBoundary - 2;

    // 左境界
    for (let i = 1; i <= leftBoundary; i++) {
      pages.push(i);
    }

    // 左省略記号
    if (showLeftEllipsis) {
      pages.push('left-ellipsis');
    }

    // 中央のページ
    for (let i = leftSibling; i <= rightSibling; i++) {
      if (i > leftBoundary && i < rightBoundary) {
        pages.push(i);
      }
    }

    // 右省略記号
    if (showRightEllipsis) {
      pages.push('right-ellipsis');
    }

    // 右境界
    for (let i = rightBoundary; i <= totalPages; i++) {
      pages.push(i);
    }

    return pages;
  }, [currentPage, totalPages, siblings, boundaries]);

  // サイズに基づくスタイル
  const getSizeClasses = () => {
    const sizes = {
      sm: 'text-sm min-w-8 h-8 px-2',
      md: 'text-base min-w-10 h-10 px-3',
      lg: 'text-lg min-w-12 h-12 px-4',
    };
    return sizes[size as keyof typeof sizes] || sizes.md;
  };

  const sizeClasses = getSizeClasses();
  const iconSizeClasses = size === 'sm' ? 'w-4 h-4' : size === 'lg' ? 'w-6 h-6' : 'w-5 h-5';

  // ページボタンのレンダリング
  const renderPageButton = (page: number | string, index: number) => {
    if (typeof page === 'string') {
      return (
        <span
          key={`${page}-${index}`}
          className={`inline-flex items-center justify-center ${sizeClasses} text-gray-400 cursor-default`}
        >
          ...
        </span>
      );
    }

    const isActive = page === currentPage;

    return (
      <button
        key={page}
        onClick={() => onPageChange(page)}
        disabled={isActive}
        aria-current={isActive ? 'page' : undefined}
        className={`
          inline-flex items-center justify-center font-medium rounded-lg
          transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
          ${sizeClasses}
          ${
            isActive
              ? 'bg-blue-600 text-white cursor-default'
              : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300 focus:ring-blue-500'
          }
        `}
      >
        {page}
      </button>
    );
  };

  return (
    <nav
      className={`flex items-center justify-center ${className}`}
      aria-label="ページネーション"
    >
      <ul className="flex items-center gap-1">
        {/* 最初のページへ */}
        {showFirstLast && (
          <li>
            <button
              onClick={() => onPageChange(1)}
              disabled={currentPage === 1}
              aria-label="最初のページへ"
              className={`
                inline-flex items-center justify-center font-medium rounded-lg
                transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
                ${sizeClasses}
                ${
                  currentPage === 1
                    ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
                    : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300 focus:ring-blue-500'
                }
              `}
            >
              <svg className={iconSizeClasses} fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
              </svg>
            </button>
          </li>
        )}

        {/* 前のページへ */}
        {showPrevNext && (
          <li>
            <button
              onClick={() => onPageChange(currentPage - 1)}
              disabled={currentPage === 1}
              aria-label="前のページへ"
              className={`
                inline-flex items-center justify-center font-medium rounded-lg
                transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
                ${sizeClasses}
                ${
                  currentPage === 1
                    ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
                    : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300 focus:ring-blue-500'
                }
              `}
            >
              <svg className={iconSizeClasses} fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
              </svg>
            </button>
          </li>
        )}

        {/* ページ番号 */}
        {pageNumbers.map((page, index) => (
          <li key={`${page}-${index}`}>
            {renderPageButton(page, index)}
          </li>
        ))}

        {/* 次のページへ */}
        {showPrevNext && (
          <li>
            <button
              onClick={() => onPageChange(currentPage + 1)}
              disabled={currentPage === totalPages}
              aria-label="次のページへ"
              className={`
                inline-flex items-center justify-center font-medium rounded-lg
                transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
                ${sizeClasses}
                ${
                  currentPage === totalPages
                    ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
                    : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300 focus:ring-blue-500'
                }
              `}
            >
              <svg className={iconSizeClasses} fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
              </svg>
            </button>
          </li>
        )}

        {/* 最後のページへ */}
        {showFirstLast && (
          <li>
            <button
              onClick={() => onPageChange(totalPages)}
              disabled={currentPage === totalPages}
              aria-label="最後のページへ"
              className={`
                inline-flex items-center justify-center font-medium rounded-lg
                transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
                ${sizeClasses}
                ${
                  currentPage === totalPages
                    ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
                    : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300 focus:ring-blue-500'
                }
              `}
            >
              <svg className={iconSizeClasses} fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
              </svg>
            </button>
          </li>
        )}
      </ul>
    </nav>
  );
};

// シンプルなページネーション(前へ/次へのみ)
export const SimplePagination: React.FC<{
  currentPage: number;
  totalPages: number;
  onPageChange: (page: number) => void;
  className?: string;
}> = ({ currentPage, totalPages, onPageChange, className = '' }) => {
  return (
    <div className={`flex items-center justify-between ${className}`}>
      <button
        onClick={() => onPageChange(currentPage - 1)}
        disabled={currentPage === 1}
        className={`
          inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg
          transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
          ${
            currentPage === 1
              ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
              : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300 focus:ring-blue-500'
          }
        `}
      >
        前へ
      </button>
      
      <span className="text-sm text-gray-700">
        {currentPage} / {totalPages}
      </span>
      
      <button
        onClick={() => onPageChange(currentPage + 1)}
        disabled={currentPage === totalPages}
        className={`
          inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg
          transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
          ${
            currentPage === totalPages
              ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
              : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300 focus:ring-blue-500'
          }
        `}
      >
        次へ
      </button>
    </div>
  );
};

// ページネーションのデモコンポーネント
export const PaginationDemo: React.FC = () => {
  const [page1, setPage1] = React.useState(1);
  const [page2, setPage2] = React.useState(5);
  const [page3, setPage3] = React.useState(1);
  const [page4, setPage4] = React.useState(1);

  return (
    <div className="space-y-12">
      {/* 基本的なページネーション */}
      <div className="space-y-4">
        <h3 className="text-lg font-semibold">基本的なページネーション</h3>
        <Pagination
          currentPage={page1}
          totalPages={10}
          onPageChange={setPage1}
        />
      </div>

      {/* 多数のページ */}
      <div className="space-y-4">
        <h3 className="text-lg font-semibold">多数のページ(省略記号あり)</h3>
        <Pagination
          currentPage={page2}
          totalPages={50}
          onPageChange={setPage2}
        />
      </div>

      {/* サイズバリエーション */}
      <div className="space-y-4">
        <h3 className="text-lg font-semibold">サイズバリエーション</h3>
        <div className="space-y-4">
          <Pagination
            currentPage={1}
            totalPages={10}
            onPageChange={() => {}}
            size="sm"
          />
          <Pagination
            currentPage={1}
            totalPages={10}
            onPageChange={() => {}}
            size="md"
          />
          <Pagination
            currentPage={1}
            totalPages={10}
            onPageChange={() => {}}
            size="lg"
          />
        </div>
      </div>

      {/* カスタマイズオプション */}
      <div className="space-y-4">
        <h3 className="text-lg font-semibold">カスタマイズオプション</h3>
        <div className="space-y-4">
          <div>
            <p className="text-sm text-gray-600 mb-2">最初/最後ボタンなし</p>
            <Pagination
              currentPage={page3}
              totalPages={10}
              onPageChange={setPage3}
              showFirstLast={false}
            />
          </div>
          <div>
            <p className="text-sm text-gray-600 mb-2">前/次ボタンなし</p>
            <Pagination
              currentPage={page3}
              totalPages={10}
              onPageChange={setPage3}
              showPrevNext={false}
            />
          </div>
          <div>
            <p className="text-sm text-gray-600 mb-2">表示範囲を拡大</p>
            <Pagination
              currentPage={page3}
              totalPages={20}
              onPageChange={setPage3}
              siblings={2}
              boundaries={2}
            />
          </div>
        </div>
      </div>

      {/* シンプルなページネーション */}
      <div className="space-y-4">
        <h3 className="text-lg font-semibold">シンプルなページネーション</h3>
        <div className="max-w-sm">
          <SimplePagination
            currentPage={page4}
            totalPages={10}
            onPageChange={setPage4}
          />
        </div>
      </div>
    </div>
  );
};

export default Pagination;

使用例

// 基本的な使用例
import { Pagination, SimplePagination } from './pagination';

function App() {
  const [currentPage, setCurrentPage] = useState(1);
  const totalPages = 50;
  const itemsPerPage = 10;
  const totalItems = 500;

  return (
    <div>
      {/* 基本的なページネーション */}
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
      />

      {/* サイズ変更 */}
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
        size="sm"
      />

      {/* カスタマイズオプション */}
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
        showFirstLast={false}
        showPrevNext={true}
        siblings={2}
        boundaries={1}
      />

      {/* シンプルなページネーション */}
      <SimplePagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
      />

      {/* 表示情報付き */}
      <div className="flex items-center justify-between">
        <p className="text-sm text-gray-700">
          全{totalItems}件中 {(currentPage - 1) * itemsPerPage + 1}-
          {Math.min(currentPage * itemsPerPage, totalItems)}件を表示
        </p>
        <Pagination
          currentPage={currentPage}
          totalPages={totalPages}
          onPageChange={setCurrentPage}
        />
      </div>
    </div>
  );
}