ページネーションPaginationExperimental

複数ページに分かれた一覧や検索結果を移動するためのナビゲーションです。ページ番号、省略記号、前後移動に加えて、件数サマリーや表示件数の選択と組み合わせて使います。

プレビュー

状態とバリエーション

状態管理あり

現在ページをアプリ側の state で管理し、必要なページ番号だけを表示します。

現在のページ 7 / 50

先頭と末尾へ移動

ページ数が多い一覧では、前後移動に加えて先頭と末尾へ直接移動する操作を追加できます。

現在のページ 6 / 24

ラベルなし操作

スペースが限られる場所では、最初・前へ・次へ・最後のラベルを視覚的に隠し、ツールチップと aria-label で意味を補います。

テーブル向け

DataTable と同じ考え方で、件数サマリー、表示件数、先頭/末尾、スマホ用ページ選択を組み合わせます。

51 - 75 / 全238件

プロパティ

表は横にスクロールできます
プロパティ初期値説明
Paginationnav-ページ送り全体のランドマーク。
PaginationLink.isActiveboolean-現在ページを示します。
PaginationFirst / PaginationLasta-必要な場合に先頭ページ、末尾ページへ移動するリンク。長い一覧やテーブルではオプションとして組み合わせます。
PaginationPrevious / PaginationNexta-前後ページへ移動するリンク。label で表示文言を差し替えられます。
PaginationEllipsisspan-省略されたページ範囲を示します。

使い方

import * as React from "react"
import { Pagination, PaginationContent, PaginationEllipsis, PaginationFirst, PaginationItem, PaginationLast, PaginationLink, PaginationNext, PaginationPrevious, Select, Tooltip, TooltipContent, TooltipTrigger } from "@gunjo/ui"

function getPageItems(currentPage: number, pageCount: number): Array<number | "ellipsis-start" | "ellipsis-end"> {
  if (pageCount <= 7) return Array.from({ length: pageCount }, (_, index) => index + 1)
  if (currentPage <= 4) return [1, 2, 3, 4, 5, "ellipsis-end", pageCount]
  if (currentPage >= pageCount - 3) return [1, "ellipsis-start", pageCount - 4, pageCount - 3, pageCount - 2, pageCount - 1, pageCount]
  return [1, "ellipsis-start", currentPage - 1, currentPage, currentPage + 1, "ellipsis-end", pageCount]
}

function PaginationControlTooltip({ label, disabledLabel, disabled, children }: { label: string; disabledLabel: string; disabled: boolean; children: React.ReactNode }) {
  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <span className="inline-flex">{children}</span>
      </TooltipTrigger>
      <TooltipContent>{disabled ? disabledLabel : label}</TooltipContent>
    </Tooltip>
  )
}

function CompactPaginationControls({
  page,
  totalPages,
  onPageChange,
  showBoundary = false,
  ariaLabel,
}: {
  page: number
  totalPages: number
  onPageChange: (page: number) => void
  showBoundary?: boolean
  ariaLabel: string
}) {
  const canPrevious = page > 1
  const canNext = page < totalPages

  return (
    <Pagination className="sm:hidden" aria-label={ariaLabel}>
      <div className="grid w-full max-w-xs grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2">
        <div className="flex items-center gap-1">
          {showBoundary ? (
            <PaginationControlTooltip label="最初のページへ" disabledLabel="すでに最初のページです" disabled={!canPrevious}>
              <PaginationFirst href="#" label={<span className="sr-only">最初のページへ</span>} aria-label="最初のページへ" aria-disabled={!canPrevious} className="h-9 w-9 gap-0 px-0" onClick={(event) => { event.preventDefault(); onPageChange(1) }} />
            </PaginationControlTooltip>
          ) : null}
          <PaginationControlTooltip label="前へ" disabledLabel="前のページはありません" disabled={!canPrevious}>
            <PaginationPrevious href="#" label={<span className="sr-only">前へ</span>} aria-label="前のページへ" aria-disabled={!canPrevious} className="h-9 w-9 gap-0 px-0" onClick={(event) => { event.preventDefault(); onPageChange(Math.max(1, page - 1)) }} />
          </PaginationControlTooltip>
        </div>
        <label className="flex min-w-0 items-center justify-center gap-1 text-sm text-muted-foreground">
          <span className="sr-only">ページを選択</span>
          <Select aria-label="ページを選択" value={String(page)} onChange={(event) => onPageChange(Number(event.target.value))} className="h-9 w-16 rounded-md py-1 text-center text-sm">
            {Array.from({ length: totalPages }, (_, index) => index + 1).map((pageNumber) => (
              <option key={pageNumber} value={pageNumber}>{pageNumber}</option>
            ))}
          </Select>
          <span aria-hidden="true">/</span>
          <span aria-hidden="true">{totalPages}</span>
        </label>
        <div className="flex items-center justify-end gap-1">
          <PaginationControlTooltip label="次へ" disabledLabel="次のページはありません" disabled={!canNext}>
            <PaginationNext href="#" label={<span className="sr-only">次へ</span>} aria-label="次のページへ" aria-disabled={!canNext} className="h-9 w-9 gap-0 px-0" onClick={(event) => { event.preventDefault(); onPageChange(Math.min(totalPages, page + 1)) }} />
          </PaginationControlTooltip>
          {showBoundary ? (
            <PaginationControlTooltip label="最後のページへ" disabledLabel="すでに最後のページです" disabled={!canNext}>
              <PaginationLast href="#" label={<span className="sr-only">最後のページへ</span>} aria-label="最後のページへ" aria-disabled={!canNext} className="h-9 w-9 gap-0 px-0" onClick={(event) => { event.preventDefault(); onPageChange(totalPages) }} />
            </PaginationControlTooltip>
          ) : null}
        </div>
      </div>
    </Pagination>
  )
}

export function PaginationExample() {
  const [page, setPage] = React.useState(2)
  const totalPages = 10
  const pages = React.useMemo(() => getPageItems(page, totalPages), [page, totalPages])

  return (
    <>
      <CompactPaginationControls page={page} totalPages={totalPages} onPageChange={setPage} ariaLabel="ページ送り" />
      <Pagination className="hidden sm:flex" aria-label="ページ送り">
        <PaginationContent>
          <PaginationItem>
            <PaginationPrevious href="#" label="前へ" aria-label="前のページへ" onClick={(event) => { event.preventDefault(); setPage((current) => Math.max(1, current - 1)) }} />
          </PaginationItem>
          {pages.map((entry) => entry === "ellipsis-start" || entry === "ellipsis-end" ? (
            <PaginationItem key={entry}><PaginationEllipsis /></PaginationItem>
          ) : (
            <PaginationItem key={entry}>
              <PaginationLink href="#" isActive={entry === page} aria-label={`${entry}ページへ移動`} onClick={(event) => { event.preventDefault(); setPage(entry) }}>
                {entry}
              </PaginationLink>
            </PaginationItem>
          ))}
          <PaginationItem>
            <PaginationNext href="#" label="次へ" aria-label="次のページへ" onClick={(event) => { event.preventDefault(); setPage((current) => Math.min(totalPages, current + 1)) }} />
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </>
  )
}

使用コンポーネント

設計の判断(UIXHERO)

「いつ・なぜ使うか」の判断は、姉妹サイト UIXHERO の記事で解説しています。