# Pagination

> Numbered Pagination with ellipsis, CursorPager (previous/next with a "Showing X-Y" label) and LoadMore button with a loading state.

Source: https://docs.nasaqui.com/components/pagination

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/pagination.json
```

Three ways to move through a long list. `Pagination` shows numbered pages with ellipsis. `CursorPager` has previous and
next only and a "Showing X-Y" label, for APIs that use cursors. `LoadMore` is a button that appends the next batch.

## When to use

- `Pagination`: the total is known and users jump to a page (tables, search results).
- `CursorPager`: the API gives cursors and cannot jump.
- `LoadMore`: feeds where continuing the list matters more than position.

## When not to use

- Fewer than about 20 items: show them all.
- Steps in a flow: use a stepper.

## Import

```tsx
import { CursorPager, LoadMore, Pagination } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
import { useState } from "react";
import { Pagination } from "@fadymondy/nasaq/web";

export function Results() {
  const [page, setPage] = useState(1);
  return <Pagination page={page} pageCount={24} onPageChange={setPage} />;
}
```

## Anatomy

```
<Pagination>   <nav data-slot="pagination" aria-label>
└─ ul > li     previous · page buttons (aria-current="page") · ellipsis (data-slot="pagination-ellipsis") · next
<CursorPager>  <nav data-slot="cursor-pager">  summary text + previous / next buttons
<LoadMore>     data-slot="load-more"  (a Button)
```

## API

### Pagination (`PaginationProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `page` | `number` | required | Current page, 1-based. |
| `pageCount` | `number` | required | Total pages. |
| `onPageChange` | `(page: number) => void` | required | Called with the new page. |
| `siblings` | `number` | `1` | Pages each side of the current one. |
| `boundaries` | `number` | `1` | Pages always shown at each end. |
| `label` | `string` | "Pagination" / "ترقيم الصفحات" | Landmark name. |
| `previousLabel` / `nextLabel` | `string` | "Previous" / "Next" (Arabic by locale) | Chevron button names. |
| `pageLabel` | `(formatted: string) => string` | "Page N" / "الصفحة N" | Name of a page button. |

`getPaginationItems(page, pageCount, siblings?, boundaries?)` returns `(number | "start-ellipsis" | "end-ellipsis")[]`.

### CursorPager (`CursorPagerProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `from` / `to` | `number` | required | First and last item index on this page. |
| `total` | `number` | none | Total items when known. |
| `hasPrevious` / `hasNext` | `boolean` | required | Enable the buttons. |
| `onPrevious` / `onNext` | `() => void` | required | Handlers. |
| `loading` | `boolean` | `false` | Disable both while fetching. |
| `label` | `string` | "Pagination" / "ترقيم الصفحات" | Landmark name. |
| `previousLabel` / `nextLabel` | `ReactNode` | "Previous" / "Next" (Arabic by locale) | Button text. |
| `summary` | `ReactNode` | "Showing X-Y of Z" | Replaces the summary. |

### LoadMore

All `Button` props (`loading`, `variant`, `onClick`). Default variant `secondary`, default text "Load more" / "تحميل المزيد".

## Examples

Cursor pager:

```tsx
import { CursorPager } from "@fadymondy/nasaq/web";

export function Pager() {
  return (
    <CursorPager from={21} to={40} total={95} hasPrevious hasNext onPrevious={() => {}} onNext={() => {}} />
  );
}
```

Load more with loading state:

```tsx
import { useState } from "react";
import { LoadMore } from "@fadymondy/nasaq/web";

export function More() {
  const [loading, setLoading] = useState(false);
  return <LoadMore loading={loading} onClick={() => setLoading(true)} />;
}
```

## Accessibility

| Key | Action |
| --- | --- |
| Tab | Moves between buttons. |
| Enter / Space | Activates. |

- Each control is a `nav` landmark with a label; the current page has `aria-current="page"`. Page buttons are named "Page N".
- Labels default to English or Arabic from the Nasaq locale; pass your own for other languages.

## RTL & i18n

Chevrons are directional icons and mirror in RTL, so "previous" points right in Arabic. Page numbers and the "showing" range are formatted with Nasaq number formatting (Western digits) and the range is bidi-isolated.

## Styling & tokens

Buttons use `Button` variants; the current page uses `border-primary` and `bg-nq-selected`. `className` merges onto the nav.

## Do / Don't

- Do keep the current page announced with `aria-current`.
- Don't use it for fewer than a screenful of items.

## Related

[button](https://docs.nasaqui.com/components/button), [data-table](https://docs.nasaqui.com/components/data-table).

## Lab

https://docs.nasaqui.com/?path=/docs/components-navigation-pagination--docs

## Code

### React

```tsx
import { useState } from "react";
import { Pagination } from "@fadymondy/nasaq/web";

export function Results() {
  const [page, setPage] = useState(1);
  return <Pagination page={page} pageCount={24} onPageChange={setPage} />;
}
```

### shadcn

```tsx
import { useState } from "react";
import { Pagination } from "@/components/ui/pagination";

export function Results() {
  const [page, setPage] = useState(1);
  return <Pagination page={page} pageCount={24} onPageChange={setPage} />;
}
```

### Vue

```vue
<script setup lang="ts">
import { NqPagination } from "@fadymondy/nasaq/vue";
import { ref } from "vue";

const page = ref(1);
</script>

<template>
  <NqPagination v-model:page="page" :page-count="24" />
</template>
```

### Blade

```blade
<div class="flex flex-col items-start gap-6">
    <x-nq::pagination :page-count="24" :page="3" />
    <x-nq::pagination.cursor-pager :from="21" :to="40" :total="95" :has-previous="true" :has-next="true" />
    <x-nq::pagination.load-more />
</div>
```

### HTML + Alpine

```html
<div class="flex flex-col items-start gap-6">
    <nav data-slot="pagination" aria-label="Pagination"
    x-data="nqPagination(3, 24, 1, 1)" x-modelable="page"
    class="w-fit max-w-full">
    <ul class="flex flex-wrap items-center gap-1">
        <li>
            <button data-slot="button"
     type="button"                         aria-label="Previous" x-on:click="go(page - 1)" x-bind:disabled="page &lt;= 1" x-bind:data-disabled="page &lt;= 1 ? &#039;&#039; : undefined" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0">
        <svg data-slot="icon" aria-hidden="true" class="rtl:-scale-x-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m15 18-6-6 6-6"/>
</svg></button>
        </li>
                                    <li data-ssr><button type="button" aria-label="Page 1"                     class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 tabular-nums border-transparent">1</button></li>
                                                <li data-ssr><button type="button" aria-label="Page 2"                     class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 tabular-nums border-transparent">2</button></li>
                                                <li data-ssr><button type="button" aria-label="Page 3"  aria-current="page"                     class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 tabular-nums border-primary bg-nq-selected">3</button></li>
                                                <li data-ssr><button type="button" aria-label="Page 4"                     class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 tabular-nums border-transparent">4</button></li>
                                                <li data-ssr><button type="button" aria-label="Page 5"                     class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 tabular-nums border-transparent">5</button></li>
                                                <li data-ssr data-slot="pagination-ellipsis" class="flex size-control-sm items-center justify-center text-muted-foreground"><span aria-hidden="true">…</span><span class="sr-only">More pages</span></li>
                                                <li data-ssr><button type="button" aria-label="Page 24"                     class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 tabular-nums border-transparent">24</button></li>
                            <template x-for="item in items()" :key="item">
            <li :data-slot="typeof item === 'number' ? undefined : 'pagination-ellipsis'" :class="typeof item === 'number' ? '' : 'flex size-control-sm items-center justify-center text-muted-foreground'">
                <button type="button" x-show="typeof item === 'number'" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 tabular-nums" :class="item === page ? 'border-primary bg-nq-selected' : 'border-transparent'"
                    :aria-label="pageLabel(item)" :aria-current="item === page ? 'page' : undefined" x-text="num(item)" x-on:click="go(item)"></button>
                <span x-show="typeof item !== 'number'" class="contents"><span aria-hidden="true">…</span><span class="sr-only">More pages</span></span>
            </li>
        </template>
        <li>
            <button data-slot="button"
     type="button"                         aria-label="Next" x-on:click="go(page + 1)" x-bind:disabled="page &gt;= pageCount" x-bind:data-disabled="page &gt;= pageCount ? &#039;&#039; : undefined" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0">
        <svg data-slot="icon" aria-hidden="true" class="rtl:-scale-x-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m9 18 6-6-6-6"/>
</svg></button>
        </li>
    </ul>
</nav>
    <nav data-slot="cursor-pager" aria-label="Pagination" class="flex w-full items-center justify-between gap-3">
    <p class="text-body-sm text-muted-foreground tabular-nums"><bdi>Showing 21–40 of 95</bdi></p>
    <div class="flex items-center gap-2">
        <button data-slot="cursor-pager-previous"
     type="button"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        <svg data-slot="icon" aria-hidden="true" class="rtl:-scale-x-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m15 18-6-6 6-6"/>
</svg>            Previous</button>
        <button data-slot="cursor-pager-next"
     type="button"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        Next
            <svg data-slot="icon" aria-hidden="true" class="rtl:-scale-x-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m9 18 6-6-6-6"/>
</svg></button>
    </div>
</nav>
    <button data-slot="load-more"
     type="button"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Load more</button>
</div>
```
