Pagination
Dzieli długi zbiór wyników na strony i pozwala się między nimi poruszać — przewidywalnie, z linkami, które można udostępnić, i z poczuciem, jak duży jest cały zbiór.
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
export default function PaginationDemo() {
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="?page=1" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=1">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=2" isActive>
2
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=3">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=12">12</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="?page=3" />
</PaginationItem>
</PaginationContent>
</Pagination>
)
}Anatomia
- 1PoprzedniaCofa o jedną stronę. Na małych ekranach zwija się do samej strzałki.
- 2Link do stronyPrawdziwy link do strony — najlepiej z numerem strony w adresie URL.
- 3Bieżąca stronaWyróżniona obrysem i odczytywana jako bieżąca strona.
- 4WielokropekZastępuje pominięte strony.
- 5NastępnaPrzechodzi o jedną stronę dalej.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/pagination.jsonUżycie
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="?page=1" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=2" isActive>
2
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="?page=3" />
</PaginationItem>
</PaginationContent>
</Pagination>Strony to prawdziwe linki. Umieść numer strony w adresie URL (?page=3), aby wyniki dało się udostępnić, dodać do zakładek, przywrócić przyciskiem Wstecz i zaindeksować w wyszukiwarkach.
Przykłady
Domyślna
Bieżąca strona dostaje isActive, który przełącza ją na styl z obrysem i ustawia aria-current="page". Poniżej breakpointu sm linki Poprzednia i Następna zwijają się do samych strzałek.
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
export default function PaginationDemo() {
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="?page=1" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=1">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=2" isActive>
2
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=3">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=12">12</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="?page=3" />
</PaginationItem>
</PaginationContent>
</Pagination>
)
}Kontrolowana z zakresem stron
Gdy stron jest dużo, pokaż pierwszą, ostatnią i bieżącą wraz z sąsiednimi; luki zastąp komponentem PaginationEllipsis.
Strona 7 z 20
"use client"
import * as React from "react"
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
/** First, last, the current page and its neighbours — gaps become ellipses. */
function pageRange(current: number, total: number): (number | "gap")[] {
const pages = new Set([1, total, current - 1, current, current + 1])
const sorted = [...pages]
.filter((p) => p >= 1 && p <= total)
.sort((a, b) => a - b)
return sorted.flatMap((page, i) =>
i > 0 && page - sorted[i - 1] > 1 ? (["gap", page] as const) : [page]
)
}
export default function PaginationControlled() {
const total = 20
const [page, setPage] = React.useState(7)
function go(event: React.MouseEvent, next: number) {
event.preventDefault()
setPage(Math.min(total, Math.max(1, next)))
}
return (
<div className="flex flex-col items-center gap-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={`?page=${page - 1}`}
aria-disabled={page === 1}
onClick={(event) => go(event, page - 1)}
/>
</PaginationItem>
{pageRange(page, total).map((item, i) =>
item === "gap" ? (
<PaginationItem key={`gap-${i}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={item}>
<PaginationLink
href={`?page=${item}`}
isActive={item === page}
onClick={(event) => go(event, item)}
>
{item}
</PaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationNext
href={`?page=${page + 1}`}
aria-disabled={page === total}
onClick={(event) => go(event, page + 1)}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<p className="text-sm text-muted-foreground tabular-nums">
{`Strona ${page} z ${total}`}
</p>
</div>
)
}Kompaktowa
Gdy brakuje miejsca albo konkretna strona jest mniej ważna niż kierunek, pokaż pozycję jako tekst między dwoma linkami z ikonami. Każdemu z tych linków nadaj aria-label.
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
} from "@/components/ui/pagination"
export default function PaginationCompact() {
return (
<Pagination>
<PaginationContent className="gap-3">
<PaginationItem>
<PaginationLink
href="?page=2"
size="icon-sm"
aria-label="Przejdź do poprzedniej strony"
>
<ChevronLeftIcon />
</PaginationLink>
</PaginationItem>
<PaginationItem className="text-sm text-muted-foreground tabular-nums">
Strona <span className="font-medium text-foreground">3</span> z 12
</PaginationItem>
<PaginationItem>
<PaginationLink
href="?page=4"
size="icon-sm"
aria-label="Przejdź do następnej strony"
>
<ChevronRightIcon />
</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}Stopka tabeli
W tabelach danych paginacji towarzyszy wybór liczby wierszy na stronie i podsumowanie zakresu (26–50 z 237). Po zmianie tej liczby wróć do pierwszej strony.
"use client"
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Pagination,
PaginationContent,
PaginationItem,
} from "@/components/ui/pagination"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const sizes = [
{ label: "10", value: "10" },
{ label: "25", value: "25" },
{ label: "50", value: "50" },
]
export default function PaginationTableFooter() {
const total = 237
const [size, setSize] = React.useState("25")
const [page, setPage] = React.useState(1)
const perPage = Number(size)
const pages = Math.ceil(total / perPage)
const from = (page - 1) * perPage + 1
const to = Math.min(total, page * perPage)
return (
<div className="flex w-full max-w-xl flex-wrap items-center justify-between gap-4 rounded-xl border bg-card px-4 py-3 text-sm shadow-raised">
<div className="flex items-center gap-2 text-muted-foreground">
<span id="rows-per-page">Wierszy na stronę</span>
<Select
items={sizes}
value={size}
onValueChange={(value) => {
setSize(value as string)
setPage(1)
}}
>
<SelectTrigger
size="sm"
className="w-18"
aria-labelledby="rows-per-page"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sizes.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<Pagination className="mx-0 w-auto">
<PaginationContent className="gap-3">
<PaginationItem className="text-muted-foreground tabular-nums">
{`${from}–${to} z ${total}`}
</PaginationItem>
<PaginationItem className="flex gap-1">
<Button
variant="outline"
size="icon-sm"
aria-label="Poprzednia strona"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
<ChevronLeftIcon />
</Button>
<Button
variant="outline"
size="icon-sm"
aria-label="Następna strona"
disabled={page === pages}
onClick={() => setPage(page + 1)}
>
<ChevronRightIcon />
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)
}Wytyczne
Kiedy używać
- Dla zbiorów wyników, które się przeszukuje i filtruje, i do których się wraca: tabel, list w panelach administracyjnych, wyników wyszukiwania, archiwów.
- Gdy liczy się pozycja w zbiorze albo trzeba dotrzeć do końca bez wczytywania wszystkiego, co jest przed nim.
Kiedy nie używać
- Dla strumieni treści, które się przegląda, a nie przeszukuje — lepiej sprawdzi się nieskończone przewijanie lub przycisk Wczytaj więcej.
- Gdy elementów jest mniej niż około 25 — pokaż wszystkie na jednej stronie.
- Dla kroków procesu — użyj wskaźnika kroków; strony sugerują równorzędne, niezależne od siebie części.
Paginacja, „Wczytaj więcej” czy nieskończone przewijanie
Zachowanie
Trzymaj kontrolkę w tym samym miejscu na każdej stronie — pod wynikami, a przy długich listach opcjonalnie także nad nimi. Po zmianie strony przewiń do początku wyników i zachowaj filtry oraz sortowanie. Pokaż łączną liczbę wyników, jeśli ją znasz — ułatwia decyzję, czy zawęzić wyszukiwanie.
Dostępność
- Punkt orientacyjny.
Paginationrenderuje<nav>z etykietą Paginacja — w języku ustawionym przez LocaleProvider. Jeśli na stronie są dwie paginacje (nad wynikami i pod nimi), nadaj im różne etykiety, np. Paginacja, góra i Paginacja, dół. - Bieżąca strona.
isActiveustawiaaria-current="page", więc bieżąca strona jest odczytywana przez czytniki ekranu, a nie tylko wyróżniona wizualnie. - Linki kierunkowe mają etykiety Poprzednia strona i Następna strona — także wtedy, gdy widać tylko strzałki.
- Wielokropki ukrywają swoją ikonę przed technologiami wspomagającymi, a zamiast niej odczytywany jest tekst Więcej stron — w języku ustawionym przez
LocaleProvider. - Wyłączone skrajne linki. Na pierwszej i ostatniej stronie ustaw
aria-disabledna linku Poprzednia lub Następna i zostaw go na miejscu, aby układ nie przeskakiwał.
| Klawisz | Działanie |
|---|---|
Tab | Przenosi fokus kolejno po linkach do stron. |
Enter | Otwiera link z fokusem. |
Dokumentacja API
Pagination
Punkt orientacyjny <nav>. Przyjmuje wszystkie propsy elementu <nav>.
PaginationContent i PaginationItem
PaginationContent to element <ul>, który rozmieszcza pozycje; PaginationItem to każdy pojedynczy <li>.
PaginationLink
Renderuje element <a> ostylowany jak Button z prfct.
PaginationPrevious i PaginationNext
PaginationEllipsis
Renderuje <span> z ikoną ⋯, która zastępuje pominięte strony. Ikona jest ukryta przed technologiami wspomagającymi; zamiast niej odczytywany jest wizualnie ukryty tekst Więcej stron.