Pagination
Atom
Pagination divide listas longas em páginas navegáveis. Ele dá controle e contexto de posição; não substitui busca nem filtro.
Use Pagination quando a pessoa precisa percorrer um conjunto grande e estável de itens — resultados de busca, audit log, histórico de menções. A página atual deve ser inequívoca (aria-current="page") e os controles de anterior/próximo sempre visíveis.
Exemplos copiáveis
React
React
import * as React from "react"
type PulsoPaginationProps = {
page: number
totalPages: number
onPageChange: (page: number) => void
label?: string
}
function pageRange(page: number, totalPages: number): (number | "ellipsis")[] {
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1)
const middle = [page - 1, page, page + 1].filter((p) => p > 1 && p < totalPages)
const range: (number | "ellipsis")[] = [1]
if (middle[0] > 2) range.push("ellipsis")
range.push(...middle)
if (middle[middle.length - 1] < totalPages - 1) range.push("ellipsis")
range.push(totalPages)
return range
}
export function PulsoPagination({ page, totalPages, onPageChange, label = "Paginação" }: PulsoPaginationProps) {
const base =
"inline-flex h-9 min-w-9 items-center justify-center rounded-[var(--radius-md)] px-2 text-sm transition-colors hover:bg-[var(--muted)] focus-visible:outline focus-visible:outline-[var(--ring-width)] focus-visible:outline-offset-2 focus-visible:outline-[var(--ring)] disabled:pointer-events-none disabled:opacity-50"
return (
<nav role="navigation" aria-label={label} className="mx-auto flex w-full justify-center">
<ul className="flex flex-row items-center gap-1">
<li>
<button type="button" className={base} disabled={page <= 1} onClick={() => onPageChange(page - 1)}>
‹ <span className="hidden sm:inline">Anterior</span>
</button>
</li>
{pageRange(page, totalPages).map((item, index) =>
item === "ellipsis" ? (
<li key={`ellipsis-${index}`} aria-hidden="true" className="flex size-9 items-center justify-center text-[var(--fg-muted)]">
…
</li>
) : (
<li key={item}>
<button
type="button"
aria-current={item === page ? "page" : undefined}
className={`${base} ${item === page ? "border border-[var(--border-strong)] bg-[var(--card)] font-medium text-[var(--fg)]" : "text-[var(--fg-muted)]"}`}
onClick={() => onPageChange(item)}
>
{item}
</button>
</li>
)
)}
<li>
<button type="button" className={base} disabled={page >= totalPages} onClick={() => onPageChange(page + 1)}>
<span className="hidden sm:inline">Próxima</span> ›
</button>
</li>
</ul>
</nav>
)
}<PulsoPagination page={4} totalPages={12} onPageChange={setPage} />Quando usar
- Listas longas e estáveis onde posição importa: audit log, histórico, resultados de busca.
- Tabelas operacionais com ordenação onde a pessoa volta a páginas específicas.
- Conjuntos cuja contagem total é conhecida e útil de comunicar.
Quando não usar
- Feeds em tempo real; use scroll contínuo com indicador de freshness.
- Listas curtas (uma página) — não renderize paginação com página única.
- Como substituto de filtro ou busca quando o conjunto é grande demais para percorrer.
Acessibilidade
- Use
navcomrole="navigation"earia-label. - Marque a página atual com
aria-current="page". - Ellipsis é decorativa:
aria-hidden="true"com alternativa para leitores de tela se necessário. - Anterior/Próxima precisam de rótulo textual (visível ou
aria-label), nunca só o glifo. - Hit target mínimo de 44×44px em contexto mobile (
--touch-target).
Regras
- Página atual sempre visível e inequívoca — nunca dependa só de cor.
- Anterior/Próxima sempre presentes; desabilitados nos extremos, nunca ocultos.
- Mostre primeira e última página; comprima o meio com ellipsis a partir de 8 páginas.
- Não use cor de risco em paginação.
- Preserve a página na URL (
?page=) para deep-link e voltar do navegador.
Last updated on