# Componentes accesibles: Paginación

> Guía paso a paso para crear una paginación compatible con teclado y lista para lectores de pantalla, con ARIA correcta, etiquetas claras y ejemplos en Next.js.

*By Micaela Avigliano — Frontend & Accessibility Engineer · Publicado 9 ago 2025*

[Read the original on micaavigliano.com](https://micaavigliano.com/es/blog/componentes-accesibles-paginacion)

---

Hoy vamos a ver cómo crear una paginación de cero y hacerla accesible y reutilizable. Espero que les sirva y me dejen sus comentarios al final del post!  

**Github**: [https://github.com/micaavigliano/accessible-pagination](https://github.com/micaavigliano/accessible-pagination)  
**Proyecto**: [https://accessible-pagination.netlify.app/](https://accessible-pagination.netlify.app/)  

#### Custom hook para pedir data

## Bloque de código

```
const useFetch = <T,>(
  url: string,
  currentPage: number = 0,
  pageSize: number = 20) => {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<boolean>(false);
  useEffect(() => {
    const fetchData = async() => {
      setLoading(true);
      setError(false);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('network response failed')
        }
        const result: T = await response.json() as T;
        setData(result)
      } catch (error) {
        setError(true)
      } finally {
        setLoading(false);
      }
    };
    fetchData()
  }, [url, currentPage, pageSize]);
  return {
    data,
    loading,
    error,
  }
};
```

1.  Vamos a generar un custom hook con un **generic type**. Esto nos va a permitir especificar el tipo de dato esperado cuando se usa este hook.
2.  Se esperarán 3 parámetros. Uno para url de donde vamos a fetchear la data, `currentPage` que es la página donde estamos y por default es 0 y `pageSize` que es el número de items que vamos a tener por página y por default es 20 (pueden cambiarle este valor).
3.  En nuestro estado `const [data, setData] = useState<T | null>(null);` le pasamos el `generic type T` ya que a medida que lo usemos para diferentes peticiones de data vamos a esperar diferentes tipos de datos.

#### Paginación

Para que una paginación sea accesible debemos tener en cuenta los siguientes puntos:

-   El foco debe moverse por todos los elementos interactivos de la páginación y tener un indicador visible para asegurar una buena interacción con los lectores de pantalla debemos utilizar correctamente las regiones, propiedades y estados de manera correcta
-   La páginación debe estar agrupada dentro de un tag `<nav>` y contener un `aria-label` que la identifique como una paginación per se.
-   Cada item dentro de la paginación debe contener un `aria-setsize` y un `aria-posinset`. Ahora, ¿para qué sirven? Bueno, `aria-setsize` sirve para calcular el total de items dentro de la lista de la paginación. El lector de pantalla lo anunciará de la siguiente manera:
    
    captura de pantalla del anuncio del voiceover: lista de 1859 elementos
    
      
    `aria-posinset` sirve para calcular la posición del item dentro de la totalidad de items en la páginación. El lector de pantalla lo anunciará de la siguiente manera:
    
    captura de pantalla del anuncio del voiceover: página 1 de 93
    
      
    
-   Cada item debe tener un `aria-label` para poder identificar a qué página vamos a ir si presionamos sobre ese botón.
-   Tener botones para ir al siguiente/previo elemento y cada uno de estos botones debe tener su aria-label correspondiente.
-   Si nuestra paginación contiene una ellipsis, la misma debe tener correctamente marcada con un `aria-label`.
-   Cada vez que vamos a una nueva página, el screen reader debe anunciar en qué página estamos y cuántos items nuevos hay de la siguiente manera.
    
    captura de pantalla del voiceover del lector de pantalla que anuncia: página 3 cargada. Mostrando 20 elementos
    
      
    

Para poder llegar a esto vamos a codearlo de la siguiente manera:

## Bloque de código

```
const [statusMessage, setStatusMessage] = useState<string>("");
useEffect(() => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
    if (!loading) {
      setStatusMessage(`Page ${currentPage} loaded. Displaying ${data?.near_earth_objects.length || 0} items.`);
    }
  }, [currentPage, loading]);
```

Cuando la página deje de cargar, vamos a setear un nuevo mensaje con nuestra currentPage y la longitud del nuevo array que estamos cargando.  
Ahora sí! Pasemos a ver cómo esta estructurado el código en el archivo `pagination.tsx`

## Bloque de código

```
interface PaginationProps {
  currentPage: number;
  totalPages: number;
  nextPage: () => void;
  prevPage: () => void;
  goToPage: (page: number) => void;
}
```

-   **currentPage** se va a referir a la página actual. Esta misma la vamos a manejar con estamos en el componente donde deseemos utilizar la paginación de la siguiente manera: `const [currentPage, setCurrentPage] = useState<number>(1);`.
-   **totalPages** se refiere al total de items a mostrar que contiene la API.
-   **nextPage** esta función nos permitirá ir a la siguiente página y actualizar nuestro estado currentPage de la siguiente manera:

## Bloque de código

```
const handlePageChange = (newPage: number) => {
  setCurrentPage(newPage); 
};
const nextPage = () => {
  if (currentPage < totalPages) {
    handlePageChange(currentPage + 1);
  }
};
```

-   **prevPage** esta función nos permitirá ir a la página previa a nuestra página actual y actualizar nuestro estado `currentPage`.

## Bloque de código

```
const prevPage = () => {
  if (currentPage > 1) {
    handlePageChange(currentPage - 1);
  }
};
```

-   **goToPage** esta función va a necesitar un parámetro numérico y es la función que cada item va a tener para poder ir a la página deseada. Vamos a hacerla funcionar de la siguiente manera:

## Bloque de código

```
const handlePageChange = (newPage: number) => {
  setCurrentPage(newPage); 
};
```

Para que nuestra paginación cobre vida nos falta un paso más, ¡crear el array que vamos a iterar en nuestra lista! Para eso debemos seguir los siguientes pasos:

1.  Crear una función, en este caso la llamaré `getPageNumbers`.
2.  Crear variables para el primer y el último item del listado.
3.  Crear una variable para la elipsis del lado izquierdo. Por decisión propia, mi elipsis se va a ubicar luego del cuarto elemento de la lista.
4.  Crear una variable para la elipsis del lado derecho. Por decisión propia, mi elipsis se va a ubicar previo a tres items en la lista.
5.  Crear una función que nos devuelva un array donde estén centrados siempre 5 items, la página actual, dos items previos y dos items subsiguientes. En caso de necesitamos, vamos a excluir a la primera y última página

## Bloque de código

```
const pagesAroundCurrent = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2].filter(page => page > firstPage && page < lastPage);
```

6.  Para nuestra última variable, vamos a crear un array que contenga todas las variables previamente creadas.
7.  Por último, vamos a filtrar los elementos `null` y devolver el array.  
      
    

Este array es el que vamos a recorrer para obtener el listado de items en nuestra paginación de la siguiente manera:

## Bloque de código

```
<ol className="flex gap-3">
  {pageNumbers.map((number) => {
    const isEllipsis = number === "left-ellipsis" || number === "right-ellipsis"
    if (isEllipsis) {
      return (
        <li
          key={number}
          className="relative top-5"
          aria-label="Ellipsis, more pages between"
        >
          <span aria-hidden>…</span>
        </li>
      )
    }
    const page = Number(number)
    const isCurrent = currentPage === page
    return (
      <li
        key={`page-${number}`}
        aria-setsize={totalPages}
        aria-posinset={typeof number === "number" ? page : undefined}
      >
        <button
          onClick={() => goToPage(page)}
          className={isCurrent ? "underline underline-offset-3 border-zinc-300" : undefined}
          aria-label={`Go to page ${page}`}
          aria-current={isCurrent ? "page" : undefined}
        >
          {page}
        </button>
      </li>
    )
  })}
</ol>
```

Y hasta acá cómo realizar una paginación reutilizable y accesible! Personalmente, aprendí a realizar una páginación de cero a los golpes porque tuve que implementarla en un live coding, espero que mi experiencia le sea de ayuda para su carrera y puedan implementar y ¡hasta mejorarla!  
  

Saludos!  
Mica
