---
title: Pagination
description: A navigation component that allows users to navigate through multiple pages of content with numbered page buttons and previous/next controls
---

```tsx
'use client';

import { useState } from 'react';
import { Pagination } from '@/ui/Pagination';

export function PaginationDemo() {
  const [currentPage, setCurrentPage] = useState(1);

  return (
    <div className="space-y-4">
      <p className="text-center text-sm text-grey">Current Page: {currentPage}</p>
      <Pagination
        defaultPage={currentPage}
        count={10}
        labels={{
          previous: 'Previous',
          next: 'Next',
        }}
        onChange={setCurrentPage}
      />
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/pagination.json
```

Or manually, copy the following code into `/ui/pagination.tsx`:

```tsx
'use client';

import clsx from 'clsx';
import { Fragment, type FunctionComponent, type HTMLAttributes } from 'react';

import {
  showMobileEllipsis,
  generatePagination,
  hidePageButtonForMobile,
} from './Pagination.helper';
import { Button } from './buttons/Button';
import { Icon } from '@clubmed/trident-icons';
import type { ScrollerLabels } from './types/ScrollerLabels';

export interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
  /**
   * The current page number (must be >= 1)
   */
  defaultPage: number;
  /**
   * The total pages number
   */
  count: number;
  /**
   * A callback function to handle page changes.
   */
  onChange: (page: number) => void;
  /**
   * Additional class names for the previous & next buttons
   */
  buttonClassName?: string;
  /**
   * Additional class names for the label of the previous & next buttons
   */
  buttonLabelClassName?: string;
  /**
   * Additional class names
   */
  className?: string;
  /**
   * UI Labels
   */
  labels: ScrollerLabels;
  /**
   * A pagination factory to generate an array of Pagination elements (pageNumber, ellipsis, next button, previous button).
   */
  paginationFactory?: typeof generatePagination;
}

const EllipsisDesktop = () => {
  return (
    <li key="desktop-ellipsis">
      <div
        aria-controls="page-content"
        className="text-base mx-8 ml-4 hidden size-48 items-center justify-center text-b3 font-semibold text-black md:flex"
      >
        &#8230;
      </div>
    </li>
  );
};

const EllipsisMobile = () => {
  return (
    <li key="mobile-ellipsis">
      <div
        aria-controls="page-content"
        className="text-base mx-8 ml-4 flex size-48 items-center justify-center text-b3 font-semibold text-black md:hidden"
      >
        &#8230;
      </div>
    </li>
  );
};

export const Pagination: FunctionComponent<PaginationProps> = ({
  defaultPage,
  buttonClassName = 'md:mr-32',
  buttonLabelClassName = 'md:flex',
  className,
  labels = {},
  onChange,
  paginationFactory = generatePagination,
  count,
  ...props
}) => {
  const pagination = paginationFactory(defaultPage, count);

  return (
    <nav
      {...props}
      data-name="Pagination"
      role="navigation"
      aria-label="pagination navigation"
      className={clsx('flex justify-center border-t-sand', className)}
    >
      <ul className="mt-12 flex">
        {pagination.map((page, index) => {
          switch (page.type) {
            case 'pageNumber':
              return (
                <Fragment key={`pagination-pageNumber-${index}`}>
                  {page.value === count && showMobileEllipsis(page.value, defaultPage, count) && (
                    <EllipsisMobile />
                  )}
                  <li
                    className={clsx('mr-2', {
                      'hidden md:flex': hidePageButtonForMobile(page.value, defaultPage, count),
                    })}
                  >
                    {page.value === defaultPage ? (
                      <Button
                        aria-current="page"
                        className="text-white"
                        color="black"
                        variant="circle"
                      >
                        {page.value.toString()}
                      </Button>
                    ) : (
                      <Button
                        className={clsx('size-48 items-center', {
                          'mx-8 flex shrink-0 cursor-pointer justify-center self-start':
                            index === pagination.length - 1,
                        })}
                        onClick={() => onChange(page.value)}
                        color="white"
                        variant="circle"
                      >
                        {page.value.toString()}
                      </Button>
                    )}
                  </li>
                  {page.value === 1 && showMobileEllipsis(page.value, defaultPage, count) && (
                    <EllipsisMobile />
                  )}
                </Fragment>
              );

            case 'ellipsis':
              return <EllipsisDesktop key={`pagination-ellipsis-${index}`} />;

            case 'previousButton':
              return (
                <li key="pagination-labels.previous" className={clsx(buttonClassName)}>
                  <button
                    data-action="previous"
                    aria-controls="page-content"
                    aria-label={`page ${defaultPage - 1}`}
                    className={clsx(
                      'text:black text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',
                      {
                        'pointer-events-none disabled:text-grey': page.disabled,
                      },
                    )}
                    disabled={page.disabled}
                    onClick={() => onChange(defaultPage - 1)}
                    type="button"
                  >
                    <Icon name="ArrowDefaultLeft" width="30px" />
                    <span
                      className={clsx(
                        'ml-4 hidden border-b',
                        { 'border-black': !page.disabled },
                        buttonLabelClassName,
                      )}
                    >
                      {labels.previous}
                    </span>
                  </button>
                </li>
              );

            case 'nextButton':
              return (
                <li className={clsx(buttonClassName)} key="pagination-labels.next">
                  <button
                    aria-controls="page-content"
                    aria-label={`page ${defaultPage + 1}`}
                    className={clsx(
                      'text:black mb-5 text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',
                      {
                        'pointer-events-none disabled:text-grey': page.disabled,
                      },
                    )}
                    data-action="next"
                    disabled={page.disabled}
                    onClick={() => onChange(defaultPage + 1)}
                    type="button"
                  >
                    <span
                      className={clsx(
                        'ml-4 hidden border-b',
                        { 'border-black': !page.disabled },
                        buttonLabelClassName,
                      )}
                    >
                      {labels.next}
                    </span>
                    <Icon name="ArrowDefaultRight" width="30px" />
                  </button>
                </li>
              );

            default:
              return null;
          }
        })}
      </ul>
    </nav>
  );
};
```

## Usage

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { Pagination } from '@/docs/lib/ui/pagination';

export function Component() {
  const [currentPage, setCurrentPage] = useState(1);

  return (
    <Pagination
      defaultPage={currentPage}
      count={10}
      labels={{
        previous: 'Previous',
        next: 'Next',
      }}
      onChange={setCurrentPage}
    />
  );
}
```

## API Reference

### Pagination

| Prop                   | Type                        | Default              | Description                                                                                        |
| ---------------------- | --------------------------- | -------------------- | -------------------------------------------------------------------------------------------------- |
| `defaultPage`          | `number`                    | -                    | **Required**. The current page number (must be >= 1). Controls which page is visually highlighted. |
| `count`                | `number`                    | -                    | **Required**. The total number of pages available for navigation.                                  |
| `onChange`             | `(page: number) => void`    | -                    | **Required**. Callback function invoked when user navigates to a different page.                   |
| `labels`               | `ScrollerLabels`            | `{}`                 | **Required**. Object containing `previous` and `next` string labels for navigation buttons.        |
| `buttonClassName`      | `string`                    | `"md:mr-32"`         | Additional CSS classes applied to the previous and next button containers.                         |
| `buttonLabelClassName` | `string`                    | `"md:flex"`          | Additional CSS classes applied to the labels within the previous and next buttons.                 |
| `className`            | `string`                    | `undefined`          | Additional CSS classes applied to the root navigation element.                                     |
| `paginationFactory`    | `typeof generatePagination` | `generatePagination` | Custom factory function to generate pagination elements. Useful for custom pagination logic.       |

The component also accepts all standard HTML `<nav>` element attributes via `HTMLAttributes<HTMLElement>`.

## Notes

- **Responsive Design**: The pagination automatically adjusts for mobile and desktop views. On mobile, fewer page buttons are shown with ellipsis indicators to save space.
- **Accessibility**: The component includes proper ARIA attributes including `role="navigation"`, `aria-label="pagination navigation"`, `aria-controls="page-content"`, and `aria-current="page"` for the active page.
- **Visual States**: The active page is highlighted with a black circular button, while inactive pages use white circular buttons. Disabled previous/next buttons appear in grey.
- **Ellipsis Display**: When there are many pages, ellipsis (…) indicators appear to show there are more pages available. Different ellipsis are shown for mobile vs desktop breakpoints.
- **Page Button Hiding**: On mobile devices, some page buttons are automatically hidden to keep the UI compact while maintaining context of the current position.
- **Navigation Buttons**: Previous and next buttons include both icon and text label. The text labels are hidden on mobile but visible on desktop (controlled by `buttonLabelClassName`).
- **Custom Pagination Logic**: The `paginationFactory` prop allows you to provide a custom function for generating the pagination structure, enabling advanced use cases like server-side pagination or custom page ranges.
- **Data Attributes**: The component has `data-name="Pagination"` for testing and debugging purposes.
- **Client Component**: This is a client component (uses "use client" directive) as it requires interactivity for page navigation.
