Componentes accesibles: Paginación
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
Proyecto: https://accessible-pagination.netlify.app/
Custom hook para pedir data
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,
}
};
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
Vamos a esperar 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).
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 unaria-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:

aria-pointset
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:

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
correspondienteSi 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.
Para poder llegar a esto vamos a codearlo de la siguiente manera:
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
El componente va a requerir de cinco props
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:
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
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:
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:
Crear una función, en este caso la llamaré
getPageNumbers
Crear variables para el primer y el último item del listado.
Crea 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.
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.
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
const pagesAroundCurrent = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2].filter(page => page > firstPage && page < lastPage);
Para nuestra última variable, vamos a crear un array que contenga todas las variables previamente creadas.
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:
<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 <3