# Pagination

> Page navigator for tabular and paged content. Ships a React layer for app code and a data-uikit-pagination adapter for the vanilla runtime - both speak the same DOM contract so the layers stay swappable.

- Category: navigation
- Status: stable (since 0.3.0)
- A11y pattern: https://www.w3.org/WAI/ARIA/apg/patterns/
- Tokens: --font-mono, --foreground-primary, --foreground-secondary, --cta-background, --cta-foreground, --border-width-thin
- Playground: https://design.freecodecamp.org/playground#pagination
- npm dependencies: `react@>=18 <20`
- Registry dependencies: [theme](https://design.freecodecamp.org/registry/theme.md)
- Files:
  - `Pagination.tsx` → `src/ui/pagination/Pagination.tsx` (raw: https://design.freecodecamp.org/registry/pagination/Pagination.tsx)
  - `pagination.css` → `src/ui/pagination/pagination.css` (raw: https://design.freecodecamp.org/registry/pagination/pagination.css)

## Install (copy source)

1. Ensure the theme is installed once per project - tokens.css + base.css imported globally, fonts available. See https://design.freecodecamp.org/registry/theme.md and https://design.freecodecamp.org/registry/starter.md.
2. Copy the files below into `src/ui/pagination/` (adjust to your project layout) and import the CSS once from your global stylesheet, e.g. `@import './ui/pagination/pagination.css';`.
3. Colors, spacing and type come from tokens - tailor the component by editing the copied source; recolour by editing tokens.css, not the component CSS.

## Usage

Pagination renders a compact page navigator with ellipsis markers for
large ranges, next/previous buttons, and keyboard arrow navigation.
The React layer is a controlled component - you own `page` state and
pass `onPageChange`. The vanilla runtime exposes the same DOM through
`[data-uikit-pagination]` and fires `uikit:pagination-change` events
so server-rendered pages can page without React.

## Keyboard

| Key    | Action        |
| ------ | ------------- |
| `←`    | Previous page |
| `→`    | Next page     |
| `Home` | First page    |
| `End`  | Last page     |

## Accessibility

Renders as `<nav aria-label="pagination">`; the current page carries
`aria-current="page"`. Ellipsis markers are non-interactive and
`aria-hidden`. Keyboard navigation is handled by the vanilla adapter
on the root - the React layer leaves key handling to consumers so
routing libraries can own the shortcut map.

## Example

```tsx
import { Pagination } from './ui/pagination/Pagination';

<Pagination
  count={120}
  pageSize={10}
  page={2}
  onPageChange={(page) => setPage(page)}
/>
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `count` | `number` | yes | - |  |
| `pageSize` | `number` | yes | - |  |
| `page` | `number` | yes | - |  |
| `siblingCount` | `number` | no | `1` |  |
| `onPageChange` | `((page: number) => void)` | no | - |  |
| `prevLabel` | `ReactNode` | no | `Previous` |  |
| `nextLabel` | `ReactNode` | no | `Next` |  |

## Source: Pagination.tsx

```tsx
import React, { forwardRef } from 'react';

export type PaginationEntry = number | 'ellipsis';

export function paginationRange(
  page: number,
  pageCount: number,
  siblingCount = 1
): PaginationEntry[] {
  if (pageCount <= 1) {
    return pageCount === 1 ? [1] : [];
  }
  const totalNumbers = siblingCount * 2 + 5;
  if (pageCount <= totalNumbers) {
    const out: PaginationEntry[] = [];
    for (let i = 1; i <= pageCount; i += 1) out.push(i);
    return out;
  }
  const leftSibling = Math.max(page - siblingCount, 1);
  const rightSibling = Math.min(page + siblingCount, pageCount);
  const showLeftEllipsis = leftSibling > 2;
  const showRightEllipsis = rightSibling < pageCount - 1;
  const result: PaginationEntry[] = [1];
  if (showLeftEllipsis) {
    result.push('ellipsis');
  } else {
    for (let i = 2; i < leftSibling; i += 1) result.push(i);
  }
  for (let i = leftSibling; i <= rightSibling; i += 1) {
    if (i !== 1 && i !== pageCount) result.push(i);
  }
  if (showRightEllipsis) {
    result.push('ellipsis');
  } else {
    for (let i = rightSibling + 1; i < pageCount; i += 1) result.push(i);
  }
  result.push(pageCount);
  return result;
}

export interface PaginationProps extends Omit<
  React.HTMLAttributes<HTMLElement>,
  'onChange'
> {
  count: number;
  pageSize: number;
  page: number;
  siblingCount?: number;
  onPageChange?: (page: number) => void;
  prevLabel?: React.ReactNode;
  nextLabel?: React.ReactNode;
}

export const Pagination = forwardRef<HTMLElement, PaginationProps>(
  (
    {
      count,
      pageSize,
      page,
      siblingCount = 1,
      onPageChange,
      prevLabel = 'Previous',
      nextLabel = 'Next',
      className = '',
      ...rest
    },
    ref
  ) => {
    const pageCount = Math.max(1, Math.ceil(count / Math.max(1, pageSize)));
    const entries = paginationRange(page, pageCount, siblingCount);
    const classes = ['pagination', className].filter(Boolean).join(' ');
    const go = (target: number) => {
      if (target < 1 || target > pageCount || target === page) return;
      onPageChange?.(target);
    };
    return (
      <nav
        ref={ref}
        className={classes}
        role='navigation'
        aria-label='pagination'
        {...rest}
      >
        <button
          type='button'
          data-part='prev'
          className='pagination__btn pagination__btn--prev'
          disabled={page <= 1}
          aria-label='Previous page'
          onClick={() => go(page - 1)}
        >
          {prevLabel}
        </button>
        <ul className='pagination__list'>
          {entries.map((entry, i) =>
            entry === 'ellipsis' ? (
              <li
                key={`e-${i}`}
                data-part='ellipsis'
                className='pagination__ellipsis'
                aria-hidden='true'
              >
                …
              </li>
            ) : (
              <li key={entry} className='pagination__item'>
                <button
                  type='button'
                  data-part='page'
                  className='pagination__btn'
                  aria-current={entry === page ? 'page' : undefined}
                  aria-label={`Page ${entry}`}
                  onClick={() => go(entry)}
                >
                  {entry}
                </button>
              </li>
            )
          )}
        </ul>
        <button
          type='button'
          data-part='next'
          className='pagination__btn pagination__btn--next'
          disabled={page >= pageCount}
          aria-label='Next page'
          onClick={() => go(page + 1)}
        >
          {nextLabel}
        </button>
      </nav>
    );
  }
);
Pagination.displayName = 'Pagination';
```

## Source: pagination.css

```css
.pagination {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}
.pagination__list {
  display: flex;
  align-items: center;
  gap: 4px;
  list-style: none;
  padding: 0;
  margin: 0;
}
.pagination__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 32px;
  padding: 0 8px;
  font-family: inherit;
  font-size: inherit;
  color: var(--foreground-secondary);
  background: transparent;
  border: var(--border-width-thin) solid var(--foreground-secondary);
  cursor: pointer;
  transition:
    color 120ms,
    background-color 120ms,
    border-color 120ms;
}
.pagination__btn:hover:not(:disabled) {
  color: var(--background-primary);
  background: var(--foreground-primary);
  border-color: var(--foreground-primary);
}
.pagination__btn[aria-current='page'] {
  color: var(--cta-foreground);
  background: var(--cta-background);
  border-color: var(--cta-background);
  cursor: default;
}
.pagination__btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.pagination__ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 32px;
  color: var(--foreground-secondary);
}
```

## HTML / vanilla variant

```html
<nav class="pagination" aria-label="Pagination">
  <ul class="pagination__list">
    <li><button class="pagination__btn" disabled>‹</button></li>
    <li><button class="pagination__btn" aria-current="page">2</button></li>
    <li><span class="pagination__ellipsis">…</span></li>
  </ul>
</nav>
```

Interactive behaviours for plain HTML come from the vanilla runtime (data-uikit-* attributes): https://design.freecodecamp.org/registry/vanilla.md - or download https://design.freecodecamp.org/cdn/uikit.global.js once and self-host it (do not hotlink).

## For coding agents

This library is distributed as copyable source, not an npm package. Start at https://design.freecodecamp.org/registry/starter.md, discover components via https://design.freecodecamp.org/llms.txt, and copy files into the consuming project. Keep token names intact; recolour by editing the copied tokens.css.
