# Pagination

A pager whose window is a fixed handful of slots, so a 200-page set draws what a 20-page one does.

```slint
import { Pagination } from "@glint/components/pagination.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 620px;
    height: 200px;
    background: Tokens.color-background;

    in-out property <int> page: 4;

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        HorizontalLayout {
            alignment: center;

            Pagination {
                accessible-label: "Invoices";
                total: 12;
                current <=> root.page;
            }
        }

        Text {
            text: "Showing page " + (root.page + 1) + " of 12.";
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }
    }
}
```

## Usage

```slint
import { Pagination } from "@glint/components/pagination.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component AppWindow inherits Window {
    width: 520px;
    height: 140px;
    background: Tokens.color-background;

    in-out property <int> page: 0;

    VerticalLayout {
        padding: 24px;
        alignment: center;

        HorizontalLayout {
            alignment: center;

            Pagination {
                accessible-label: "Search results";
                total: 8;
                current <=> root.page;
                changed(page) => { debug("load page", page); }
            }
        }
    }
}
```

`total` is how many pages there are, counting from one. `current` is which one is showing, counting from **zero** — the index you would use against an array, not the number drawn on the button. It is two-way, so a pager can be driven from outside as well as clicked, and `changed` fires with the new index once a step or a number moves it.

The pager draws nothing itself but the controls. Loading the page is yours: the component reports where the reader went and stops there.

## Examples

### The window

A set too long to list windows itself. The pager keeps the first page, the last page, the `sibling-count` pages either side of the current one, and an ellipsis standing for each run in between — so the number of slots is fixed and a 200-page set draws the same handful of buttons a 20-page one does. Below the window size the set is drawn whole, because an ellipsis standing for one page saves nothing.

The ellipsis is not a control: it goes nowhere, so it takes no tab stop and leaves no node in the accessibility tree.

```slint
import { Pagination } from "@glint/components/pagination.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 660px;
    height: 260px;
    background: Tokens.color-background;

    in-out property <int> near-start: 1;
    in-out property <int> middle: 96;
    in-out property <int> near-end: 198;

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        HorizontalLayout {
            alignment: center;
            Pagination {
                accessible-label: "Near the start";
                total: 200;
                current <=> root.near-start;
            }
        }

        HorizontalLayout {
            alignment: center;
            Pagination {
                accessible-label: "In the middle";
                total: 200;
                current <=> root.middle;
            }
        }

        HorizontalLayout {
            alignment: center;
            Pagination {
                accessible-label: "Near the end";
                total: 200;
                current <=> root.near-end;
            }
        }
    }
}
```

### How many siblings

`sibling-count` is how many pages are drawn either side of the current one, and it is what the window is measured from: the window is both ends, both ellipses and the current page with its siblings. Raising it widens the pager everywhere, not only in the middle.

```slint
import { Pagination } from "@glint/components/pagination.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 680px;
    height: 240px;
    background: Tokens.color-background;

    in-out property <int> tight: 50;
    in-out property <int> wide: 50;

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        Text {
            text: "sibling-count: 0";
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }
        HorizontalLayout {
            alignment: center;
            Pagination {
                accessible-label: "One page either side";
                total: 100;
                sibling-count: 0;
                current <=> root.tight;
            }
        }

        Text {
            text: "sibling-count: 2";
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }
        HorizontalLayout {
            alignment: center;
            Pagination {
                accessible-label: "Two pages either side";
                total: 100;
                sibling-count: 2;
                current <=> root.wide;
            }
        }
    }
}
```

### Taking a section away

`show-numbers` and `show-steps` each remove one half of the pager. Numbers alone are the simple pager; steps alone are the icons-only pair a data table wears under its rows. A step with nowhere to go dims, refuses the click and leaves the tab order, which is [Button](/docs/components/button)’s own `disabled` doing the work.

```slint
import { Pagination } from "@glint/components/pagination.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 620px;
    height: 260px;
    background: Tokens.color-background;

    in-out property <int> simple: 0;
    in-out property <int> steps-only: 3;

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        Text {
            text: "Numbers alone — no Previous or Next steps.";
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }
        HorizontalLayout {
            alignment: center;
            Pagination {
                accessible-label: "Simple pager";
                total: 6;
                show-steps: false;
                current <=> root.simple;
            }
        }

        Text {
            text: "Steps alone.";
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }
        HorizontalLayout {
            alignment: center;
            Pagination {
                accessible-label: "Compact pager";
                total: 6;
                show-numbers: false;
                current <=> root.steps-only;
            }
        }
    }
}
```

### Naming the steps

The two steps are icon-only buttons, so their names are strings the component ships and a call site overrides — `previous-label` and `next-label`, both `@tr(…)` by default. The page numbers name themselves from their own position.

```slint
import { Pagination } from "@glint/components/pagination.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 620px;
    height: 180px;
    background: Tokens.color-background;

    in-out property <int> page: 2;

    VerticalLayout {
        padding: 24px;
        alignment: center;

        HorizontalLayout {
            alignment: center;

            Pagination {
                accessible-label: "Chapters";
                total: 7;
                current <=> root.page;
                previous-label: "Previous chapter";
                next-label: "Next chapter";
            }
        }
    }
}
```

## API Reference

### Properties

| Property         | Type         | Default                | Description                                                                      |
| ---------------- | ------------ | ---------------------- | -------------------------------------------------------------------------------- |
| `total`          | `in int`     | `1`                    | Total number of pages (1-based count).                                           |
| `current`        | `in-out int` | `0`                    | Two-way; the active page (0-indexed internally).                                 |
| `previous-label` | `in string`  | `@tr("Previous page")` | Name of the icon-only step to the previous page; override to translate.          |
| `next-label`     | `in string`  | `@tr("Next page")`     | Name of the icon-only step to the next page; override to translate.              |
| `sibling-count`  | `in int`     | `1`                    | Pages drawn either side of the current one once the set is windowed.             |
| `show-numbers`   | `in bool`    | `true`                 | Draw the page numbers. Off leaves the steps alone — the icons-only pager.        |
| `show-steps`     | `in bool`    | `true`                 | Draw the previous / next steps. Off leaves the numbers alone — the simple pager. |

### Callbacks

| Callback       | Description                             |
| -------------- | --------------------------------------- |
| `changed(int)` | Fired with the new page index on click. |

## Accessibility

- **A navigation landmark.** The pager carries `accessible-role: navigation` and publishes `accessible-item-count` as `total`. Naming it is the call site’s — set `accessible-label`, so a page with a pager over its table and another under it tells the two apart.
- **Every destination is a named button.** A page number announces itself `Page 4`, carries its index and reports whether it is the current page (`accessible-item-selected`), so a screen reader says where the reader is without the visual highlight.
- **The steps carry their own names and their enabled state.** Previous and Next are icon-only, so `previous-label` and `next-label` are what they are announced by; a step with nowhere to go reports itself unavailable rather than disappearing.
- **The ellipsis is not announced.** It stands for a run that was elided and goes nowhere, so it is not a control and leaves no node. What tells a reader where in the range they are is the landmark’s item count and each button’s own index.
- **One tab stop per destination.** The pager is a set of independent destinations rather than one control with a selection inside it, so every live button in it is its own stop and Enter or Space activates the focused one. That is deliberately not the single roving stop [Tabs](/docs/components/tabs) and [NavigationMenu](/docs/components/navigation-menu) use: a roving stop belongs to one control whose arrows move a selection inside it.
