Ir al contenido
Evaluar mi proyecto
Componentes · NavegaciónDisponible

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.

navegacion-paginacion.tsx123 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/navegacion-paginacion

Ejemplo de uso de Paginación compacta

Después de copiar navegacion-paginacion.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
totalPages?numberOpcional.
initialPage?numberOpcional.
onChange?(page: number) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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.

VariableValor 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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)