# Pagination

Moves between the pages of a long list: numbered buttons in a bar, Previous and Next around a select of pages, or a range readout for a table.

Block: `smbk-pagination`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `type` | `numbers`, `select`, `range` | `numbers` | Figma: Default (numbers), Select, Range. |
| `borderless` | on / off | off | The numbers bar without its border. Figma: Border=No. |

Parts: `smbk-pagination__list`, `smbk-pagination__item`, `smbk-pagination__page`, `smbk-pagination__gap`, `smbk-pagination__status`, `smbk-pagination__previous`, `smbk-pagination__next`, `smbk-pagination__select`, `smbk-pagination__label`, `smbk-pagination__wide-only`, `smbk-pagination__narrow-only`

## Markup

```html
<nav aria-label="Pagination" class="smbk-pagination smbk-pagination--type-numbers">
  <ul class="smbk-pagination__list">
    <li class="smbk-pagination__item">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-btn--icon-only smbk-pagination__previous"
        aria-label="Previous page"
      >
        <i
          class="ph ph-caret-left smbk-icon smbk-icon--size-md smbk-btn__icon"
          aria-hidden="true"
        ></i>
      </button>
    </li>
    <li class="smbk-pagination__item smbk-pagination__status">6 of 11</li>
    <li class="smbk-pagination__item">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-pagination__page"
        data-label="1"
        aria-label="Page 1"
      >
        <span class="smbk-btn__label">1</span>
      </button>
    </li>
    <li class="smbk-pagination__item">
      <span class="smbk-pagination__gap" aria-hidden="true">…</span>
    </li>
    <li class="smbk-pagination__item">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-pagination__page"
        data-label="5"
        aria-label="Page 5"
      >
        <span class="smbk-btn__label">5</span>
      </button>
    </li>
    <li class="smbk-pagination__item">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-pagination__page"
        data-label="6"
        aria-current="page"
        aria-label="Page 6"
      >
        <span class="smbk-btn__label">6</span>
      </button>
    </li>
    <li class="smbk-pagination__item">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-pagination__page"
        data-label="7"
        aria-label="Page 7"
      >
        <span class="smbk-btn__label">7</span>
      </button>
    </li>
    <li class="smbk-pagination__item">
      <span class="smbk-pagination__gap" aria-hidden="true">…</span>
    </li>
    <li class="smbk-pagination__item">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-pagination__page"
        data-label="10"
        aria-label="Page 10"
      >
        <span class="smbk-btn__label">10</span>
      </button>
    </li>
    <li class="smbk-pagination__item">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-pagination__page"
        data-label="11"
        aria-label="Page 11"
      >
        <span class="smbk-btn__label">11</span>
      </button>
    </li>
    <li class="smbk-pagination__item">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-btn--icon-only smbk-pagination__next"
        aria-label="Next page"
      >
        <i
          class="ph ph-caret-right smbk-icon smbk-icon--size-md smbk-btn__icon"
          aria-hidden="true"
        ></i>
      </button>
    </li>
  </ul>
</nav>
```

## React

```tsx
import { Pagination } from './design-system/components/pagination/pagination.js';

<Pagination type="numbers" page={6} count={11} pageSize={20} total={100} />
```

## Stylesheet

```css
/* smbk-pagination — Pagination. Figma "Pagination" 4253:3044 (Type: Default, Select, Range; Border: Yes, No).
 * The numbers type is a bar of ghost buttons, the current page held in the
 * pressed look, with or without its border. The select type is Previous, a
 * select of pages, Next. The range type says which rows are showing out of
 * how many, with a select of ranges, for a table's foot.
 * The current page shows by its pressed fill and a label one weight bolder, so
 * it is found without telling colors apart. Digits are tabular, every one as
 * wide as the next, so numbers line up and do not shift as they change.
 * In a narrow space the numbers bar has no room for its numbers, so it shows
 * a short form: Previous, "6 of 11", Next (the status part); the select type's
 * Previous and Next show their icons alone. */

.smbk-pagination {
  display: inline-flex;
  align-items: center;
}

.smbk-pagination__list {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  gap: var(--smbk-space-inline-xs);
  margin: 0;
  padding: var(--smbk-space-inset-xs);
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
  border-radius: var(--smbk-radius-control-lg);
  list-style: none;
}

.smbk-pagination--borderless .smbk-pagination__list {
  border-color: transparent;
}

.smbk-pagination__item {
  display: inline-flex;
}

/* At least a square, and as wide as its number needs with the inset beside it, so a page past 999 fits. Column, so the
   copy below lies under the number and not beside it. */
.smbk-pagination__page {
  flex-direction: column;
  gap: 0;
  min-width: var(--smbk-size-control-sm);
  padding: 0 var(--smbk-space-inset-xs);
  font-variant-numeric: tabular-nums;
}

/* The button is always as wide as its number at the current page's weight: an invisible copy of the number
   (`data-label`, which the wrapper writes) lies under it with no height, so a page turning current never moves the
   pages beside it. The copy is generated content, hidden, with empty words for assistive technology. */
.smbk-pagination__page[data-label]::after {
  content: attr(data-label);
  content: attr(data-label) / "";
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: var(--smbk-typography-body-sm-semibold-font-weight);
  pointer-events: none;
  user-select: none;
}

/* Figma holds the current page in the pressed fill; its label one weight bolder, the semibold of the
   same 14 px, is the code's own: the weight variants of the text styles live in the token source, not
   in Figma. Only the weight changes, so the digit sits level with the others. */
.smbk-pagination__page[aria-current="page"] {
  background: var(--smbk-color-action-ghost-background-pressed);
  font-weight: var(--smbk-typography-body-sm-semibold-font-weight);
}

.smbk-pagination__gap {
  display: inline-block;
  text-align: center;
  min-width: var(--smbk-size-control-sm);
  color: var(--smbk-color-text-subtlest);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
}

/* The short form's "6 of 11", shown only in a narrow space. */
.smbk-pagination__status {
  display: none;
  align-items: center;
  padding: 0 var(--smbk-space-inset-md);
  color: var(--smbk-color-text-default);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The select type's Previous and Next: with their words in a wide space, their icons alone in a
   narrow one, so the three fit a phone. */
.smbk-pagination .smbk-pagination__narrow-only {
  display: none;
}

@container style(--smbk-layout: narrow) {
  .smbk-pagination__item:has(> .smbk-pagination__page, > .smbk-pagination__gap) {
    display: none;
  }

  .smbk-pagination__status,
  .smbk-pagination .smbk-pagination__narrow-only {
    display: inline-flex;
  }

  .smbk-pagination .smbk-pagination__wide-only {
    display: none;
  }
}

/* ---- type: select ---- */

.smbk-pagination--type-select {
  gap: var(--smbk-space-inline-lg);
}

.smbk-pagination--type-select .smbk-pagination__select {
  width: auto;
}

.smbk-pagination__select .smbk-select__value,
.smbk-pagination__select .smbk-select__option-label {
  font-variant-numeric: tabular-nums;
}

/* ---- type: range ---- */

.smbk-pagination--type-range {
  gap: var(--smbk-space-inline-lg);
}

.smbk-pagination--type-range .smbk-pagination__select {
  width: auto;
}

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
   user's own colors. Nothing here applies outside that mode. */

@media (forced-colors: active) {
  .smbk-pagination__page[aria-current="page"] {
    forced-color-adjust: none;
    border-color: Highlight;
    background: Highlight;
    color: HighlightText;
    box-shadow: none;
  }

  .smbk-pagination__page[aria-current="page"]:focus-visible {
    outline-color: CanvasText;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "inline": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things side by side, large."
      }
    },
    "inset": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Padding inside a component, extra small."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "color": {
    "border": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      }
    },
    "action": {
      "ghost": {
        "background-pressed": {
          "$type": "color",
          "$value": "{{color.action.outline.background-pressed}}",
          "$description": "Ghost button: fill while pressed, one step past hover; also the current page in pagination."
        }
      }
    },
    "text": {
      "subtlest": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "Quietest text: hints, captions, placeholders."
      },
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      }
    }
  },
  "radius": {
    "control": {
      "lg": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of controls, large."
      }
    }
  },
  "size": {
    "control": {
      "sm": {
        "$type": "dimension",
        "$value": "{{size.32}}",
        "$description": "Height of a small control: 32 comfortable, 28 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    }
  },
  "typography": {
    "body-sm-semibold": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 600,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      },
      "$description": "Body SM in semibold: small button labels."
    },
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  }
}
```

## Purpose

Moves the reader between the pages of a list too long to show at once, and tells them where they are in it. The numbers type lets them jump anywhere; the select type is for long lists where a row of numbers would not fit.

## Anatomy

- The block, a navigation landmark.
- Numbers type: the list part, a bordered bar holding the previous part, one page part per shown page, a gap part where pages are skipped, and the next part. Beside them the status part, "6 of 11", shown in place of the numbers in a narrow space; markup written by hand includes it, as an item of the list right after the previous part. Each page part carries its number in `data-label` as well, so a number turning bolder moves nothing beside it.
- Select type: the previous part, the select part listing every page, and the next part, with no bar. Previous and Next are each there twice, with their words (the wide-only part) and as icons alone (the narrow-only part); the layout switch shows one of each.
- Range type: a label part saying "Showing", the select part listing the row ranges, and a label part with the total, for a table's foot.
- Given each page's address, the page, previous and next parts are links drawn as the same buttons, not buttons. Previous on the first page and Next on the last are then links with no address.

## When to use

- Search results, tables and archives split into pages.

## When not to use

- Feeds that grow as the reader scrolls. Use a "load more" [button](button.md).
- Steps in a form. Use a progress indicator and Next buttons.
- Fewer than two pages. Show nothing.

## Behavior

The numbers bar can drop its border, which the table's foot does. The range type's select is the small one unless `selectSize` says otherwise, and as wide as its value, so every control in the foot is the same height. The current page is held in the pressed look, its number one weight bolder, and cannot be pressed again. Page numbers use digits of equal width, so they line up and do not shift as they change. A page button is a square up to two digits and grows with a longer number, so a page past 999 fits. In a narrow space, where a row of numbers would run off the screen, the numbers bar shows its short form instead: Previous, the page out of how many ("6 of 11"), and Next; the select type's Previous and Next show their icons alone. The form follows the layout switch, so a wrapper can force either. Previous is disabled on the first page and Next on the last; they stay in the tab order (aria-disabled), so pressing Next onto the last page keeps focus on it rather than dropping it to the top of the page. With many pages the numbers collapse to the first, the last, and a few around the current one, with a gap between; how many around the current one is a prop.

Numbers are written the way the page's language writes them: 1,116 in English, and in another language given `locale`. That holds for the page numbers and their names, the counts and the range's choices. Words the page gives itself, such as `countText`, receive the bare numbers, to write as they please.

The pages can be buttons or links. As buttons, a press calls `onChange` with the page and the page changes the list in place. Given each page's address (`href`, a function from a page's number to its address), every page and Previous and Next are links to their pages instead, which work with no script at all. They look the same, the current page included. Previous on the first page and Next on the last are links with no address, dimmed and out of the Tab order: a press loads another page anyway, so there is no focus to keep. In the select and range types, choosing in the select goes to that page's address, which needs the page's script.

Give addresses when each page has an address of its own: pages built ahead of time as files, and pages a search engine should follow. Keep `onChange` when the list changes in place, such as a table filtered and sorted on the page.

## Accessibility

### What it does for you

- The landmark is named "Pagination" by default.
- The current page carries the current-page marker, and every number button is named "Page N" so the announcement is not just a digit. It differs from the others by its weight as well as its fill, so it is found without telling colors apart.
- In the short form the page out of how many is plain text between the two buttons, read in that order.
- The arrow buttons are icon-only and carry their names.
- In the select type the select is named "Page". Its Previous and Next keep their names when they show their icons alone ("Previous page", "Next page"); the form not shown is hidden from everyone, so each is read once.
- Previous and Next stay in the tab order when disabled, so pressing Next onto the last page keeps focus on it. In the links form they are left out of it instead, since every press loads a new page.
- In the links form, the current page's link carries the current-page marker, and Previous and Next say which way they lead to a search engine as well (`rel` of prev and next).
- Every word it says by itself, such as the buttons' names, the select's name and the counts, keeps its English unless the page gives its own, so a page in another language can change it.
- In a forced-color mode, the current page is drawn in the system's highlight colors.

### What you must do

- Pass another name for the landmark when a page has two.
- Give the words in the page's own language when the page is not in English, and the language itself (`locale`), so its numbers follow.

### How to test

- Tab through it: every number button, Previous and Next is a stop with the focus ring showing. On the last page, press Next: focus stays on it.
- Press Enter or Space on a number: that page opens, and it is held in the pressed look.
- With a screen reader, it is a navigation landmark named "Pagination", each number is read as "Page N", and the current one as current.
- With text at 200% and the page 320 pixels wide, the numbers bar shows its short form, "6 of 11", with Previous and Next.
- With thousands of pages, on the last one: every number stays inside its button.
- In a forced-color mode, the current page is still told from the others.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the next button, link or the select. Previous and Next stay in the order when disabled, except in the links form. |
| Enter or Space | Presses the focused button: opens that page, or goes to the previous or next one. In the links form, Enter follows the focused link. |

## Usage rules

### Do

- Reflect the page in the address, so a reload or a shared link lands on the same page.
- Give each page's address when the pages are built ahead of time or should be found by a search engine.

### Don't

- Show a numbers bar for hundreds of pages; use the select type.
- Use the range type outside a table.
