Pagination
Navegación entre páginas con enlaces anterior y siguiente, números y elipsis.
Nuevo1'use client';23import * as React from 'react';4import { Pagination } from '@/components/ui/pagination';56const TOTAL_PAGES = 8;78function pageList(current: number): (number | 'ellipsis')[] {9 const pages = new Set([1, TOTAL_PAGES, current - 1, current, current + 1]);10 const sorted = [...pages]11 .filter((page) => page >= 1 && page <= TOTAL_PAGES)12 .sort((a, b) => a - b);1314 const result: (number | 'ellipsis')[] = [];15 let previous = 0;16 for (const page of sorted) {17 if (page - previous > 1) result.push('ellipsis');18 result.push(page);19 previous = page;20 }21 return result;22}2324export function Default() {25 const [page, setPage] = React.useState(4);2627 return (28 <Pagination>29 <Pagination.Content className="flex-wrap justify-center">30 <Pagination.Item>31 <Pagination.Previous32 disabled={page === 1}33 onClick={() => setPage((current) => Math.max(1, current - 1))}34 className="max-sm:w-9 max-sm:px-0 max-sm:[&>span]:sr-only"35 />36 </Pagination.Item>37 {pageList(page).map((item, index) =>38 item === 'ellipsis' ? (39 <Pagination.Item key={`ellipsis-${index}`}>40 <Pagination.Ellipsis />41 </Pagination.Item>42 ) : (43 <Pagination.Item key={item}>44 <Pagination.Link isActive={item === page} onClick={() => setPage(item)}>45 {item}46 </Pagination.Link>47 </Pagination.Item>48 ),49 )}50 <Pagination.Item>51 <Pagination.Next52 disabled={page === TOTAL_PAGES}53 onClick={() => setPage((current) => Math.min(TOTAL_PAGES, current + 1))}54 className="max-sm:w-9 max-sm:px-0 max-sm:[&>span]:sr-only"55 />56 </Pagination.Item>57 </Pagination.Content>58 </Pagination>59 );60}
Instalación
Copia y pega el siguiente código en tu proyecto.
1import * as React from 'react';2import { cn } from '../lib/cn';34type IconProps = React.SVGProps<SVGSVGElement> & {5 size?: number | string;6};78function ChevronLeftIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {9 return (10 <svg11 xmlns="http://www.w3.org/2000/svg"12 width={size}13 height={size}14 viewBox="0 0 24 24"15 fill="none"16 stroke="currentColor"17 strokeWidth={strokeWidth}18 strokeLinecap="round"19 strokeLinejoin="round"20 aria-hidden="true"21 {...props}22 >23 <path d="m15 6-6 6 6 6" />24 </svg>25 );26}2728function ChevronRightIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {29 return (30 <svg31 xmlns="http://www.w3.org/2000/svg"32 width={size}33 height={size}34 viewBox="0 0 24 24"35 fill="none"36 stroke="currentColor"37 strokeWidth={strokeWidth}38 strokeLinecap="round"39 strokeLinejoin="round"40 aria-hidden="true"41 {...props}42 >43 <path d="m9 6 6 6-6 6" />44 </svg>45 );46}4748function MoreHorizontalIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {49 return (50 <svg51 xmlns="http://www.w3.org/2000/svg"52 width={size}53 height={size}54 viewBox="0 0 24 24"55 fill="none"56 stroke="currentColor"57 strokeWidth={strokeWidth}58 strokeLinecap="round"59 strokeLinejoin="round"60 aria-hidden="true"61 {...props}62 >63 <circle cx="5" cy="12" r="1" />64 <circle cx="12" cy="12" r="1" />65 <circle cx="19" cy="12" r="1" />66 </svg>67 );68}6970interface PaginationProps extends React.ComponentProps<'nav'> {71 label?: string;72}7374type PaginationContentProps = React.ComponentProps<'ul'>;75type PaginationItemProps = React.ComponentProps<'li'>;7677interface PaginationLinkProps extends React.ComponentProps<'a'> {78 isActive?: boolean;79 disabled?: boolean;80 size?: 'default' | 'icon';81}8283interface PaginationPreviousProps extends Omit<PaginationLinkProps, 'size'> {84 iconOnly?: boolean;85}8687interface PaginationNextProps extends Omit<PaginationLinkProps, 'size'> {88 iconOnly?: boolean;89}9091interface PaginationEllipsisProps extends React.ComponentProps<'span'> {92 label?: string;93}9495const PaginationRoot = ({96 className,97 label = 'Pagination',98 ref,99 ...props100}: PaginationProps & { ref?: React.Ref<HTMLElement> }) => {101 return (102 <nav103 ref={ref}104 aria-label={label}105 className={cn('mx-auto flex w-full justify-center', className)}106 {...props}107 />108 );109};110111PaginationRoot.displayName = 'Pagination';112113const PaginationContent = ({114 className,115 ref,116 ...props117}: PaginationContentProps & { ref?: React.Ref<HTMLUListElement> }) => {118 return (119 <ul120 ref={ref}121 className={cn('flex flex-row flex-wrap items-center justify-center gap-1', className)}122 {...props}123 />124 );125};126127PaginationContent.displayName = 'PaginationContent';128129const PaginationItem = ({130 className,131 ref,132 ...props133}: PaginationItemProps & { ref?: React.Ref<HTMLLIElement> }) => {134 return <li ref={ref} className={cn(className)} {...props} />;135};136137PaginationItem.displayName = 'PaginationItem';138139const linkClassName = (isActive: boolean, disabled: boolean, size: 'default' | 'icon') =>140 cn(141 'inline-flex h-9 cursor-pointer items-center justify-center gap-1 rounded-md text-sm font-medium transition-colors select-none',142 'hover:bg-muted ring-offset-background focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',143 size === 'icon' ? 'w-9' : 'px-3',144 isActive && 'bg-muted text-foreground border-border-interactive hover:bg-muted border',145 disabled && 'pointer-events-none opacity-40',146 );147148const PaginationLink = ({149 className,150 isActive = false,151 disabled = false,152 size = 'icon',153 href,154 children,155 ref,156 ...props157}: PaginationLinkProps & { ref?: React.Ref<HTMLAnchorElement> }) => {158 const sharedClassName = cn(linkClassName(isActive, disabled, size), className);159160 if (href === undefined) {161 return (162 <button163 ref={ref as React.Ref<HTMLButtonElement>}164 type="button"165 aria-current={isActive ? 'page' : undefined}166 disabled={disabled}167 className={sharedClassName}168 {...(props as React.ComponentProps<'button'>)}169 >170 {children}171 </button>172 );173 }174175 return (176 <a177 ref={ref}178 href={href}179 aria-current={isActive ? 'page' : undefined}180 aria-disabled={disabled || undefined}181 tabIndex={disabled ? -1 : undefined}182 className={sharedClassName}183 {...props}184 >185 {children}186 </a>187 );188};189190PaginationLink.displayName = 'PaginationLink';191192const PaginationPrevious = ({193 className,194 iconOnly = false,195 children = 'Previous',196 ...props197}: PaginationPreviousProps) => {198 return (199 <PaginationLink200 size={iconOnly ? 'icon' : 'default'}201 className={cn('pl-2.5', iconOnly && 'pl-0', className)}202 {...props}203 >204 <ChevronLeftIcon size={16} aria-hidden="true" />205 {iconOnly ? <span className="sr-only">{children}</span> : <span>{children}</span>}206 </PaginationLink>207 );208};209210PaginationPrevious.displayName = 'PaginationPrevious';211212const PaginationNext = ({213 className,214 iconOnly = false,215 children = 'Next',216 ...props217}: PaginationNextProps) => {218 return (219 <PaginationLink220 size={iconOnly ? 'icon' : 'default'}221 className={cn('pr-2.5', iconOnly && 'pr-0', className)}222 {...props}223 >224 {iconOnly ? <span className="sr-only">{children}</span> : <span>{children}</span>}225 <ChevronRightIcon size={16} aria-hidden="true" />226 </PaginationLink>227 );228};229230PaginationNext.displayName = 'PaginationNext';231232const PaginationEllipsis = ({233 className,234 label = 'More pages',235 ref,236 ...props237}: PaginationEllipsisProps & { ref?: React.Ref<HTMLSpanElement> }) => {238 return (239 <span ref={ref} className={cn('flex size-9 items-center justify-center', className)} {...props}>240 <MoreHorizontalIcon size={16} aria-hidden="true" />241 <span className="sr-only">{label}</span>242 </span>243 );244};245246PaginationEllipsis.displayName = 'PaginationEllipsis';247248const Pagination = Object.assign(PaginationRoot, {249 Content: PaginationContent,250 Item: PaginationItem,251 Link: PaginationLink,252 Previous: PaginationPrevious,253 Next: PaginationNext,254 Ellipsis: PaginationEllipsis,255});256257export { Pagination };258export type {259 PaginationContentProps,260 PaginationEllipsisProps,261 PaginationItemProps,262 PaginationLinkProps,263 PaginationNextProps,264 PaginationPreviousProps,265 PaginationProps,266};
Actualiza las rutas de importación para que coincidan con la estructura de tu proyecto.
Anatomía
1import { Pagination } from '@/components/ui/pagination';
1<Pagination>2 <Pagination.Content>3 <Pagination.Item>4 <Pagination.Previous href="#">Anterior</Pagination.Previous>5 </Pagination.Item>6 <Pagination.Item>7 <Pagination.Link href="#">1</Pagination.Link>8 </Pagination.Item>9 <Pagination.Item>10 <Pagination.Link href="#" isActive>11 212 </Pagination.Link>13 </Pagination.Item>14 <Pagination.Item>15 <Pagination.Ellipsis />16 </Pagination.Item>17 <Pagination.Item>18 <Pagination.Next href="#">Siguiente</Pagination.Next>19 </Pagination.Item>20 </Pagination.Content>21</Pagination>
Pagination.Link renderiza un ancla cuando pasas href y un botón cuando lo omites, así el mismo markup sirve para páginas con rutas y para estado del cliente. Con Next.js, envuelve las partes de enlace o cambia el ancla por next/link en tu copia del componente.Composición
Usa la siguiente composición para construir una
Pagination:Pagination└── Pagination.Content├── Pagination.Item│ └── Pagination.Previous├── Pagination.Item│ └── Pagination.Link├── Pagination.Item│ └── Pagination.Ellipsis└── Pagination.Item└── Pagination.Next
Variantes
Compacta
Anterior y siguiente solo con iconos alrededor de un indicador de página, para barras de herramientas y espacios reducidos.
1'use client';23import * as React from 'react';4import { Pagination } from '@/components/ui/pagination';56const TOTAL_PAGES = 12;78export function Compact() {9 const [page, setPage] = React.useState(3);1011 return (12 <Pagination>13 <Pagination.Content>14 <Pagination.Item>15 <Pagination.Previous16 iconOnly17 disabled={page === 1}18 onClick={() => setPage((current) => Math.max(1, current - 1))}19 />20 </Pagination.Item>21 <Pagination.Item>22 <span className="text-muted-foreground px-2 text-sm tabular-nums">23 Page {page} of {TOTAL_PAGES}24 </span>25 </Pagination.Item>26 <Pagination.Item>27 <Pagination.Next28 iconOnly29 disabled={page === TOTAL_PAGES}30 onClick={() => setPage((current) => Math.min(TOTAL_PAGES, current + 1))}31 />32 </Pagination.Item>33 </Pagination.Content>34 </Pagination>35 );36}
Referencia de API
Pagination
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | 'Pagination' | Nombre accesible del nav |
className | string | - | Clases CSS adicionales |
Pagination.Link
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | - | Renderiza un ancla; omítelo para un botón |
isActive | boolean | false | Marca la página actual con aria-current |
disabled | boolean | false | Deshabilita el control |
size | 'default' | 'icon' | 'icon' | Número cuadrado o enlace de texto con padding |
className | string | - | Clases CSS adicionales |
Pagination.Previous / Pagination.Next
| Prop | Type | Default | Description |
|---|---|---|---|
iconOnly | boolean | false | Oculta visualmente la etiqueta |
children | ReactNode | 'Previous' / 'Next' | Texto de la etiqueta |
Ambos aceptan además todas las props de
Pagination.Link excepto size.Pagination.Ellipsis
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | 'More pages' | Texto para lectores de pantalla |
className | string | - | Clases CSS adicionales |
¿Encontraste algo que mejorar?
¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.