Paginación compacta
Paginación con extremos, páginas vecinas y elipsis; callback de cambio y controles desactivados en los límites.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useState, type CSSProperties } from "react";
/** Paginación compacta. React + TypeScript, sin dependencias. Personaliza con variables --gcxg-*. */
export type NavegacionPaginacionProps = {
totalPages?: number;
initialPage?: number;
onChange?: (page: number) => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcxg-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gcxg-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gcxg-root *, .gcxg-root *::before, .gcxg-root *::after { box-sizing: border-box; }
.gcxg-root button, .gcxg-root input, .gcxg-root textarea, .gcxg-root select { font: inherit; }
.gcxg-root button, .gcxg-root a { -webkit-tap-highlight-color: transparent; }
.gcxg-root :focus-visible { outline: 2px solid var(--gcxg-accent, #25724b); outline-offset: 4px; }
.gcxg-root ::selection { background: var(--gcxg-accent, #25724b); color: #fff; }
.gcxg-root button:disabled { opacity: .5; cursor: not-allowed; }
.gcxg-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gcxg-root *, .gcxg-root *::before, .gcxg-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gcxg-root { text-align:center; }
.gcxg-pages { display:flex; align-items:center; justify-content:center; flex-wrap:wrap; gap:5px; }
.gcxg-pages>span { display:flex; align-items:center; gap:5px; }
.gcxg-pages button { display:grid; place-items:center; min-width:44px; min-height:44px; border:1px solid #d3d9d2; border-radius:7px; color:#4b594b; background:var(--gcxg-bg,#fff); cursor:pointer; font-variant-numeric:tabular-nums; }
.gcxg-pages button:hover:not(:disabled) { background:#e4ebe1; }
.gcxg-pages button[aria-current="page"] { color:#fff; background:var(--gcxg-accent,#2c5137); border-color:transparent; }
.gcxg-gap { width:20px; color:#566253; }
.gcxg-root p { margin:14px 0 0; font-size:12px; color:#566253; }`;
export function NavegacionPaginacion({
totalPages = 12,
initialPage = 1,
onChange,
className = "",
style,
}: NavegacionPaginacionProps) {
const total = Math.max(1, Math.floor(totalPages) || 1);
const [current, setCurrent] = useState(
Math.max(1, Math.min(initialPage, total)),
);
const page = Math.min(current, total);
const visible = Array.from(
new Set(
[1, page - 1, page, page + 1, total].filter((n) => n >= 1 && n <= total),
),
).sort((a, b) => a - b);
function go(next: number) {
const bounded = Math.max(1, Math.min(next, total));
setCurrent(bounded);
onChange?.(bounded);
}
return (
<nav
className={`gcxg-root ${className}`}
style={style}
aria-label="Paginación"
>
<style>{CSS}</style>
<div className="gcxg-pages">
<button
type="button"
aria-label="Página anterior"
disabled={page === 1}
onClick={() => go(page - 1)}
>
<svg
width="16"
height="16"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
>
<path d="m12 4-6 6 6 6" />
</svg>
</button>
{visible.map((n, i) => (
<span key={n}>
{i > 0 && n - visible[i - 1] > 1 && (
<span className="gcxg-gap" aria-hidden="true">
…
</span>
)}
<button
type="button"
aria-label={`Página ${n}`}
aria-current={n === page ? "page" : undefined}
onClick={() => go(n)}
>
{n}
</button>
</span>
))}
<button
type="button"
aria-label="Página siguiente"
disabled={page === total}
onClick={() => go(page + 1)}
>
<svg
width="16"
height="16"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
>
<path d="m8 4 6 6-6 6" />
</svg>
</button>
</div>
<p role="status">
Página {page} de {total}
</p>
</nav>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/navegacion-paginacionEjemplo de uso de Paginación compacta
Después de copiar navegacion-paginacion.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { NavegacionPaginacion } from "./navegacion-paginacion";
export default function Ejemplo() {
return <NavegacionPaginacion />;
}Propiedades de NavegacionPaginacion
NavegacionPaginacion acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
totalPages? | number | Opcional. |
initialPage? | number | Opcional. |
onChange? | (page: number) => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Paginación compacta con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--gcxg-fg | #202421 |
--gcxg-accent | #25724b |
--gcxg-bg | #fff |
Accesibilidad y rendimiento
El archivo tiene 123 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Paginación compacta por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Declara roles ARIA para lectores de pantalla: status.
- Es un componente de cliente de Next.js: incluye la directiva "use client".