Pagination
Pages of results with prev/next and numbered jumps. Large tables and search hits.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Table pages:
```tsx import { Pagination, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis, } from '@/components/atoms/Pagination';
<Pagination> <PaginationItem><PaginationPrevious href="?p=1" disabled /></PaginationItem> <PaginationItem><PaginationLink href="?p=1" isActive>1</PaginationLink></PaginationItem> <PaginationItem><PaginationEllipsis /></PaginationItem> <PaginationItem><PaginationNext href="?p=2" /></PaginationItem> </Pagination> ```
## Accesibilidad
- Root `nav` has `aria-label="Pagination"`; only one `isActive` link should set `aria-current="page"`. - Disable Previous on page 1 and Next on the last page.
## Cuándo no usar
- Infinite scroll feeds — use a load-more pattern, not page numbers alone. - In-panel content switching without URLs → `Tabs`.
## Criterio de uso
- Úsala cuando el usuario necesita volver a una página concreta de resultados, especialmente en tablas y búsquedas. - Deshabilita Previous en la primera página y Next en la última; no dejes enlaces que prometen una navegación inexistente. - Mantén un único enlace activo y comunica el rango o total cuando ayude a entender la posición actual.
## Gotchas
- El contenedor debe ser un `nav` con `aria-label="Pagination"` y el estado activo debe expresarse como `aria-current="page"`. - Para feeds exploratorios donde la posición exacta no importa, usa load-more o infinite scroll; no fuerces paginación numérica.
Uso
import {
Pagination, PaginationItem, PaginationLink,
PaginationPrevious, PaginationNext, PaginationEllipsis,
} from '@/components/atoms/Pagination';
<Pagination>
<PaginationItem><PaginationPrevious href="?p=1" disabled /></PaginationItem>
<PaginationItem><PaginationLink href="?p=1" isActive>1</PaginationLink></PaginationItem>
<PaginationItem><PaginationEllipsis /></PaginationItem>
<PaginationItem><PaginationNext href="?p=2" /></PaginationItem>
</Pagination>Props
isActive: booleandisabled: booleanchildren: ReactNode (required)
Gotchas
- a11y
Root nav has aria-label=Pagination; only one PaginationLink should set isActive (aria-current=page).
- react
Compose Pagination > PaginationItem > PaginationLink|Previous|Next|Ellipsis. isActive/disabled live on link-level pieces, not the root nav.
Anatomía CSS
<div class="pagination"> <span class="pagination__ellipsis"></span> <span class="pagination__item"></span> <span class="pagination__link"></span> <span class="pagination__link--active"></span> <span class="pagination__link--disabled"></span> <span class="pagination__link-icon"></span> <span class="pagination__list"></span> </div>
| Clase | Propósito |
|---|---|
pagination | root |
pagination__ellipsis | element |
pagination__item | element |
pagination__link | element |
pagination__link--active | element |
pagination__link--disabled | element |
pagination__link-icon | element |
pagination__list | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | #fafafa | background |
| all | fg | #525252 | muted.foreground |
| all | border | #e5e5e5 | border |
| all | hover-bg | #f5f5f5 | accent |
| all | hover-fg | #171717 | accent.foreground |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
background-color | var(--duration-100) | var(--easing-in-out) |
border-color | var(--duration-100) | var(--easing-in-out) |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Pagination
Page navigation: prev, page links, ellipsis, next.
Semantic <nav> + <ul> structure.
Parts: .pagination, .pagination__list, .pagination__item,
.pagination__link, .pagination__ellipsis
------------------------------------------------------------------------- */
/* ---- Nav ---- */
.pagination {
display: flex;
justify-content: center;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
}
/* ---- List ---- */
.pagination__list {
display: flex;
align-items: center;
gap: 4px;
list-style: none;
margin: 0;
padding: 0;
}
/* ---- Link ---- */
.pagination__link {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
padding: 0 8px;
border: 1px solid transparent;
border-radius: 8px;
background: none;
color: #0a0a0a;
font-family: inherit;
font-size: 12.8px;
font-weight: 500;
line-height: 1;
text-decoration: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition:
background-color 100ms cubic-bezier(0.4, 0, 0.2, 1),
border-color 100ms cubic-bezier(0.4, 0, 0.2, 1);
}
.pagination__link:hover {
background-color: #f5f5f5;
color: #171717;
}
.pagination__link:focus-visible {
outline: 2px solid var(--focus-ring-color);
outline-offset: -2px;
}
/* Active page */
.pagination__link--active {
border-color: #e5e5e5;
background-color: #fafafa;
font-weight: 600;
}
/* Disabled (prev/next at bounds) */
.pagination__link--disabled {
color: #525252;
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* ---- Nav icon ---- */
.pagination__link-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1em;
height: 1em;
}
.pagination__link-icon svg {
width: 100%;
height: 100%;
}
/* ---- Ellipsis ---- */
.pagination__ellipsis {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
color: #525252;
font-size: 12.8px;
letter-spacing: 0.04em;
}
/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
.pagination__link {
transition-duration: 0ms;
}
}
Codigo fuente
import { type ReactNode, type AnchorHTMLAttributes } from 'react'; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } const ChevronLeft = () => ( <svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <path d="M15 18l-6-6 6-6" /> </svg> ); const ChevronRight = () => ( <svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <path d="M9 18l6-6-6-6" /> </svg> ); /* ---- Pagination ---- */ export function Pagination({ children, className }: { children: ReactNode; className?: string }) { return ( <nav aria-label="Pagination" className={cn('pagination', className)}> <ul className="pagination__list">{children}</ul> </nav> ); } /* ---- Item ---- */ export function PaginationItem({ children, className }: { children: ReactNode; className?: string }) { return <li className={className}>{children}</li>; } /* ---- Link ---- */ export type PaginationLinkProps = { isActive?: boolean; disabled?: boolean; children: ReactNode; className?: string; } & AnchorHTMLAttributes<HTMLAnchorElement>; export function PaginationLink({ isActive, disabled, children, className, ...props }: PaginationLinkProps) { return ( <a aria-current={isActive ? 'page' : undefined} className={cn( 'pagination__link', isActive && 'pagination__link--active', disabled && 'pagination__link--disabled', className, )} {...props} > {children} </a> ); } /* ---- Previous ---- */ export type PaginationPreviousProps = { disabled?: boolean; text?: string; className?: string; } & AnchorHTMLAttributes<HTMLAnchorElement>; export function PaginationPrevious({ disabled, text = 'Previous', className, ...props }: PaginationPreviousProps) { return ( <PaginationLink disabled={disabled} className={className} {...props}> <span className="pagination__link-icon"><ChevronLeft /></span> {text} </PaginationLink> ); } /* ---- Next ---- */ export type PaginationNextProps = { disabled?: boolean; text?: string; className?: string; } & AnchorHTMLAttributes<HTMLAnchorElement>; export function PaginationNext({ disabled, text = 'Next', className, ...props }: PaginationNextProps) { return ( <PaginationLink disabled={disabled} className={className} {...props}> {text} <span className="pagination__link-icon"><ChevronRight /></span> </PaginationLink> ); } /* ---- Ellipsis ---- */ export function PaginationEllipsis({ className }: { className?: string }) { return <span className={cn('pagination__ellipsis', className)} aria-hidden="true">...</span>; }
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / Pagination) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
:focus-visibleon.pagination__link:focus-visible— pseudo-class not a safe Designer variant — moved to head Custom Codeselectoron.pagination__link-icon svg— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.pagination__link:focus-visible— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)@mediaon(prefers-reduced-motion: reduce)— not a Designer breakpoint — moved to head Custom Code block--focus-ring-coloron:root— token not found in tokens-nested.json — resolve upstream or the declaration stays invalid on paste--focus-ring-widthon:root— token not found in tokens-nested.json — resolve upstream or the declaration stays invalid on paste
Tras pegar: Create component → nombre Atom / Pagination → Publish.