Pagination

Page navigation links with previous, next and ellipsis.

import { … } from '@inlaylab/ivorie/pagination'
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_PAGINATION_IMPORTS } from '@inlaylab/ivorie/pagination';

@Component({
  selector: 'demo-pagination',
  imports: [IV_PAGINATION_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <nav ivPagination>
      <ul ivPaginationContent>
        <li ivPaginationItem><a ivPaginationPrevious href="#" (click)="go(page() - 1, $event)"></a></li>
        @for (p of pages; track p) {
          <li ivPaginationItem>
            <a ivPaginationLink href="#" [isActive]="p === page()" (click)="go(p, $event)">{{ p }}</a>
          </li>
        }
        <li ivPaginationItem><a ivPaginationNext href="#" (click)="go(page() + 1, $event)"></a></li>
      </ul>
    </nav>
  `,
})
export default class PaginationDemo {
  protected readonly pages = [1, 2, 3, 4, 5];
  protected readonly page = signal(2);

  protected go(next: number, event: Event) {
    event.preventDefault();
    this.page.set(Math.min(Math.max(next, 1), this.pages.length));
  }
}

Installation

CLInpm
npx ivorie add pagination

Ports the shadcn/ui Pagination. It is presentational only. You own the page state, the range and the navigation.

Usage

import { IV_PAGINATION_IMPORTS } from '@inlaylab/ivorie/pagination';
<nav ivPagination>
  <ul ivPaginationContent>
    <li ivPaginationItem><a ivPaginationPrevious href="#" (click)="prev()"></a></li>
    @for (page of pages; track page) {
      <li ivPaginationItem>
        <a ivPaginationLink href="#" [isActive]="page === currentPage" (click)="goTo(page)">{{ page }}</a>
      </li>
    }
    <li ivPaginationItem><a ivPaginationNext href="#" (click)="next()"></a></li>
  </ul>
</nav>

Examples

Ellipsis

import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
import { IV_PAGINATION_IMPORTS } from '@inlaylab/ivorie/pagination';

@Component({
  selector: 'demo-pagination-ellipsis',
  imports: [IV_PAGINATION_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <nav ivPagination>
      <ul ivPaginationContent>
        <li ivPaginationItem><a ivPaginationPrevious href="#" (click)="go(page() - 1, $event)"></a></li>
        <li ivPaginationItem>
          <a ivPaginationLink href="#" [isActive]="page() === 1" (click)="go(1, $event)">1</a>
        </li>
        @if (page() > 3) {
          <li ivPaginationItem><span ivPaginationEllipsis></span></li>
        }
        @for (p of visible(); track p) {
          <li ivPaginationItem>
            <a ivPaginationLink href="#" [isActive]="p === page()" (click)="go(p, $event)">{{ p }}</a>
          </li>
        }
        @if (page() < total - 2) {
          <li ivPaginationItem><span ivPaginationEllipsis></span></li>
        }
        <li ivPaginationItem>
          <a ivPaginationLink href="#" [isActive]="page() === total" (click)="go(total, $event)">{{ total }}</a>
        </li>
        <li ivPaginationItem><a ivPaginationNext href="#" (click)="go(page() + 1, $event)"></a></li>
      </ul>
    </nav>
  `,
})
export default class PaginationEllipsisDemo {
  protected readonly total = 20;
  protected readonly page = signal(10);
  // The current page and its neighbors, without the first and last page.
  protected readonly visible = computed(() =>
    [this.page() - 1, this.page(), this.page() + 1].filter((p) => p > 1 && p < this.total),
  );

  protected go(next: number, event: Event) {
    event.preventDefault();
    this.page.set(Math.min(Math.max(next, 1), this.total));
  }
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { IV_PAGINATION_IMPORTS } from '@inlaylab/ivorie/pagination';

@Component({
  selector: 'demo-pagination-router-links',
  imports: [RouterLink, IV_PAGINATION_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <nav ivPagination>
      <ul ivPaginationContent>
        @for (p of pages; track p) {
          <li ivPaginationItem>
            <a ivPaginationLink routerLink="/docs/pagination" [queryParams]="{ page: p }" [isActive]="p === 1">
              {{ p }}
            </a>
          </li>
        }
      </ul>
    </nav>
  `,
})
export default class PaginationRouterLinksDemo {
  protected readonly pages = [1, 2, 3];
}

Custom labels

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_PAGINATION_IMPORTS } from '@inlaylab/ivorie/pagination';

@Component({
  selector: 'demo-pagination-custom-labels',
  imports: [IV_PAGINATION_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <nav ivPagination>
      <ul ivPaginationContent>
        <li ivPaginationItem><a ivPaginationPrevious href="#" text="Prev" (click)="$event.preventDefault()"></a></li>
        <li ivPaginationItem>
          <a ivPaginationLink href="#" [isActive]="true" (click)="$event.preventDefault()">1</a>
        </li>
        <li ivPaginationItem><a ivPaginationNext href="#" text="More" (click)="$event.preventDefault()"></a></li>
      </ul>
    </nav>
  `,
})
export default class PaginationCustomLabelsDemo {}

API

Every part also accepts class.

Root <nav>. No own inputs.

ul[ivPaginationContent]

Flex list. No own inputs.

li[ivPaginationItem]

One entry. No inputs. It has no class input.

Name Type Default Description
isActive boolean false Marks the current page. Uses the outline button look. Others use ghost.
size ButtonVariants['size'] 'icon' Any button size.

a[ivPaginationPrevious]

Name Type Default Description
text string 'Previous' Label. Hidden below the sm breakpoint.

a[ivPaginationNext]

Name Type Default Description
text string 'Next' Label. Hidden below the sm breakpoint.

span[ivPaginationEllipsis]

Gap marker. No own inputs.

Accessibility

  • The root has role="navigation" and aria-label="pagination".
  • The active link has aria-current="page" and data-active.
  • Previous and Next have aria-label "Go to previous page" and "Go to next page".
  • The ellipsis is aria-hidden and holds a screen-reader-only "More pages" text.
  • Parts are native nav, ul, li and a. Give links an href or routerLink so they are focusable and keyboard-operable.

Notes

  • Selectors sit on native elements. No wrapper divs.
  • Disabled state is yours. At the first or last page, add pointer-events-none opacity-50 and set aria-disabled on the link.
  • Previous and Next chevrons flip in RTL.