Pagination

Pronto

Navegação entre páginas de uma listagem grande. Mostra contexto (página atual / total + counts), atalhos para extremos.

Usar quando

Tabela com 50+ items. Listas onde scroll infinito não cabe (auditoria, exportação). Resultados de busca.

Não usar quando

Lista pequena (<20 itens — mostre tudo). Feed contínuo (use scroll infinito). Wizard (use Stepper).

Variantes

Padrão (com count e ranges)
Compacto (só prev/next)
Mobile (input direto na página)

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Page changeclick no númeroBackground ink + texto branco no novo, sumiço suave do antigo150ms ease
Hover buttonmouseenterBackground → surfaceHover100ms ease
Disabled statepage=1 ou page=lastopacity 0.4 + cursor not-allowed nos prev/first ou next/lastinstant
Range jumpclick "..."Pula 5 páginas (ou abre input modal pra digitar destino)instant

Acessibilidade

Acessibilidade — checklist

Teclado
TabFoco nos botões em sequência
Enter / SpaceVai para a página
← / →Anterior/próximo (com role=navigation)
ARIA esperado
  • <nav aria-label="Paginação">
  • role="navigation"
  • aria-current="page" no número da página atual
  • aria-label="Ir para página X" em cada botão de número
  • aria-label="Página anterior/próxima" nos chevrons
Notas
  • aria-current="page" no número atual.
  • Disabled state em prev/first se page=1 (não esconda).
  • Em mobile: oculte numeração e mostre input "Página X de Y".
  • Sempre exiba contagem total ("1-20 de 247").

Código

'use client';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { T, RADIUS } from '@/lib/tokens';

interface Props { page: number; totalPages: number; onChange: (p: number) => void }

export function Pagination({ page, totalPages, onChange }: Props) {
  return (
    <nav aria-label="Paginação" style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
      <button onClick={() => onChange(page - 1)} disabled={page === 1} aria-label="Página anterior">
        <ChevronLeft size={14} />
      </button>
      {/* … botões de página … */}
      <button onClick={() => onChange(page + 1)} disabled={page === totalPages} aria-label="Próxima página">
        <ChevronRight size={14} />
      </button>
    </nav>
  );
}

Regras

Faça

  • aria-current="page" no número atual.
  • Mostre contagem ("1-20 de 247") sempre.
  • Disabled state em extremos (não esconda).
  • Em mobile: input direto na página + Y total.
  • Range "..." jump 5 páginas ou input para grandes.

Não faça

  • Não esconda prev/next em extremos (mostre disabled).
  • Não force scroll infinito sem dar opção de paginar.
  • Não tenha mais de 7 botões numerados visíveis.
  • Não esqueça contagem total.
  • Não use cor brass nos botões (ink/surface basta).

Tokens usados

TokenValorPapel
T.ink#0A0A0Abackground da página atual
T.surface#FFFFFFbackground dos outros
T.surfaceHover#F7F5EChover
RADIUS.md12borderRadius