AvesraAvesrabeta

Pagination

Page navigation with composable page links, previous/next buttons, and ellipsis indicators.

Import

import {
  AvPaginationImports,
  getPaginationRange,
} from '@avesra/angular';

Usage

import { Component, computed, model } from '@angular/core';
import { AvPaginationImports } from '@avesra/angular';

@Component({
  selector: 'app-pagination-basic-demo',
  imports: [AvPaginationImports],
  template: `<av-pagination class="justify-center">
  <ul av-pagination-content>
    <li av-pagination-item>
      <button
        av-pagination-prev
        [disabled]="page() === 1"
        (click)="page.set(page() - 1)"
      >
        <span av-pagination-prev-icon></span>
        <span>Previous</span>
      </button>
    </li>
    @for (p of pages(); track p) {
      <li av-pagination-item>
        <button
          av-pagination-link
          [active]="p === page()"
          (click)="page.set(p)"
        >
          {{ p }}
        </button>
      </li>
    }
    <li av-pagination-item>
      <button
        av-pagination-next
        [disabled]="page() === totalPages"
        (click)="page.set(page() + 1)"
      >
        <span>Next</span>
        <span av-pagination-next-icon></span>
      </button>
    </li>
  </ul>
</av-pagination>`,
})
export class PaginationBasicDemo {
  readonly page = model(1);
  readonly totalPages = 3;

  readonly pages = computed(() =>
    Array.from({ length: this.totalPages }, (_, index) => index + 1),
  );
}

Anatomy

Import the Pagination parts and compose them with element and attribute selectors. Optional pieces include div[av-pagination-summary] and span[av-pagination-ellipsis].

<av-pagination>
  <div av-pagination-summary>Showing 1-10 of 100 results</div>
  <ul av-pagination-content>
    <li av-pagination-item>
      <button av-pagination-prev>
        <span av-pagination-prev-icon></span>
        <span>Previous</span>
      </button>
    </li>
    <li av-pagination-item>
      <button av-pagination-link [active]="true">1</button>
    </li>
    <li av-pagination-item>
      <span av-pagination-ellipsis></span>
    </li>
    <li av-pagination-item>
      <button av-pagination-link>10</button>
    </li>
    <li av-pagination-item>
      <button av-pagination-next>
        <span>Next</span>
        <span av-pagination-next-icon></span>
      </button>
    </li>
  </ul>
</av-pagination>

Sizes

Set size on av-pagination — sm, md (default), or lg.

sm
md
lg
import { Component, computed, model } from '@angular/core';
import { AvPaginationImports } from '@avesra/angular';

@Component({
  selector: 'app-pagination-sizes-demo',
  imports: [AvPaginationImports],
  template: `<div class="flex flex-col gap-6">
  <div class="flex flex-col gap-2">
    <span class="text-xs font-medium text-muted capitalize">sm</span>
    <av-pagination class="justify-center" size="sm">
      <ul av-pagination-content>
        <li av-pagination-item>
          <button av-pagination-prev [disabled]="smPage() === 1" (click)="smPage.set(smPage() - 1)">
            <span av-pagination-prev-icon></span>
            <span>Previous</span>
          </button>
        </li>
        @for (p of pages(); track p) {
          <li av-pagination-item>
            <button av-pagination-link [active]="p === smPage()" (click)="smPage.set(p)">{{ p }}</button>
          </li>
        }
        <li av-pagination-item>
          <button av-pagination-next [disabled]="smPage() === totalPages" (click)="smPage.set(smPage() + 1)">
            <span>Next</span>
            <span av-pagination-next-icon></span>
          </button>
        </li>
      </ul>
    </av-pagination>
  </div>
  <div class="flex flex-col gap-2">
    <span class="text-xs font-medium text-muted capitalize">md</span>
    <av-pagination class="justify-center">
      <ul av-pagination-content>
        <li av-pagination-item>
          <button av-pagination-prev [disabled]="mdPage() === 1" (click)="mdPage.set(mdPage() - 1)">
            <span av-pagination-prev-icon></span>
            <span>Previous</span>
          </button>
        </li>
        @for (p of pages(); track p) {
          <li av-pagination-item>
            <button av-pagination-link [active]="p === mdPage()" (click)="mdPage.set(p)">{{ p }}</button>
          </li>
        }
        <li av-pagination-item>
          <button av-pagination-next [disabled]="mdPage() === totalPages" (click)="mdPage.set(mdPage() + 1)">
            <span>Next</span>
            <span av-pagination-next-icon></span>
          </button>
        </li>
      </ul>
    </av-pagination>
  </div>
  <div class="flex flex-col gap-2">
    <span class="text-xs font-medium text-muted capitalize">lg</span>
    <av-pagination class="justify-center" size="lg">
      <ul av-pagination-content>
        <li av-pagination-item>
          <button av-pagination-prev [disabled]="lgPage() === 1" (click)="lgPage.set(lgPage() - 1)">
            <span av-pagination-prev-icon></span>
            <span>Previous</span>
          </button>
        </li>
        @for (p of pages(); track p) {
          <li av-pagination-item>
            <button av-pagination-link [active]="p === lgPage()" (click)="lgPage.set(p)">{{ p }}</button>
          </li>
        }
        <li av-pagination-item>
          <button av-pagination-next [disabled]="lgPage() === totalPages" (click)="lgPage.set(lgPage() + 1)">
            <span>Next</span>
            <span av-pagination-next-icon></span>
          </button>
        </li>
      </ul>
    </av-pagination>
  </div>
</div>`,
})
export class PaginationSizesDemo {
  readonly smPage = model(1);
  readonly mdPage = model(1);
  readonly lgPage = model(1);
  readonly totalPages = 3;

  readonly pages = computed(() =>
    Array.from({ length: this.totalPages }, (_, index) => index + 1),
  );
}

Disabled

Set disabled on button[av-pagination-prev], button[av-pagination-next], or button[av-pagination-link] to prevent interaction.

import { Component, computed, model } from '@angular/core';
import { AvPaginationImports } from '@avesra/angular';

@Component({
  selector: 'app-pagination-disabled-demo',
  imports: [AvPaginationImports],
  template: `<av-pagination class="justify-center">
  <ul av-pagination-content>
    <li av-pagination-item>
      <button av-pagination-prev disabled>
        <span av-pagination-prev-icon></span>
        <span>Previous</span>
      </button>
    </li>
    @for (p of pages(); track p) {
      <li av-pagination-item>
        <button
          av-pagination-link
          [active]="p === page()"
          (click)="page.set(p)"
        >
          {{ p }}
        </button>
      </li>
    }
    <li av-pagination-item>
      <button av-pagination-next disabled>
        <span>Next</span>
        <span av-pagination-next-icon></span>
      </button>
    </li>
  </ul>
</av-pagination>`,
})
export class PaginationDisabledDemo {
  readonly page = model(1);
  readonly totalPages = 3;

  readonly pages = computed(() =>
    Array.from({ length: this.totalPages }, (_, index) => index + 1),
  );
}

Simple (Previous / Next)

A minimal pattern with only previous and next controls plus a summary line.

1 to 5 of 50 invoices
import { Component, computed, model } from '@angular/core';
import { AvPaginationImports } from '@avesra/angular';

@Component({
  selector: 'app-pagination-simple-prev-next-demo',
  imports: [AvPaginationImports],
  template: `<av-pagination class="w-full">
  <div av-pagination-summary>
    {{ startItem() }} to {{ endItem() }} of {{ totalItems }} invoices
  </div>
  <ul av-pagination-content>
    <li av-pagination-item>
      <button
        av-pagination-prev
        [disabled]="page() === 1"
        (click)="page.set(page() - 1)"
      >
        <span av-pagination-prev-icon></span>
        <span>Prev</span>
      </button>
    </li>
    <li av-pagination-item>
      <button
        av-pagination-next
        [disabled]="page() === totalPages"
        (click)="page.set(page() + 1)"
      >
        <span>Next</span>
        <span av-pagination-next-icon></span>
      </button>
    </li>
  </ul>
</av-pagination>`,
})
export class PaginationSimplePrevNextDemo {
  readonly page = model(1);
  readonly totalPages = 10;
  readonly itemsPerPage = 5;
  readonly totalItems = 50;

  readonly startItem = computed(() => (this.page() - 1) * this.itemsPerPage + 1);
  readonly endItem = computed(() =>
    Math.min(this.page() * this.itemsPerPage, this.totalItems),
  );
}

Controlled

Drive the current page with a model() signal and update it from (click) handlers on page links and prev/next buttons.

Showing 1-10 of 120 results
import { Component, computed, model } from '@angular/core';
import {
  getPaginationRange,
  AvPaginationImports,
} from '@avesra/angular';

@Component({
  selector: 'app-pagination-controlled-demo',
  imports: [AvPaginationImports],
  template: `<av-pagination>
  <div av-pagination-summary>
    Showing {{ startItem() }}-{{ endItem() }} of {{ totalItems }} results
  </div>
  <ul av-pagination-content>
    <li av-pagination-item>
      <button
        av-pagination-prev
        [disabled]="page() === 1"
        (click)="page.set(page() - 1)"
      >
        <span av-pagination-prev-icon></span>
        <span>Previous</span>
      </button>
    </li>
    @for (item of range(); track $index) {
      <li av-pagination-item>
        @if (item === 'ellipsis') {
          <span av-pagination-ellipsis></span>
        } @else {
          <button
            av-pagination-link
            [active]="item === page()"
            (click)="page.set(item)"
          >
            {{ item }}
          </button>
        }
      </li>
    }
    <li av-pagination-item>
      <button
        av-pagination-next
        [disabled]="page() === totalPages"
        (click)="page.set(page() + 1)"
      >
        <span>Next</span>
        <span av-pagination-next-icon></span>
      </button>
    </li>
  </ul>
</av-pagination>`,
})
export class PaginationControlledDemo {
  readonly page = model(1);
  readonly totalPages = 12;
  readonly itemsPerPage = 10;
  readonly totalItems = 120;

  readonly range = computed(() => getPaginationRange(this.page(), this.totalPages));
  readonly startItem = computed(() => (this.page() - 1) * this.itemsPerPage + 1);
  readonly endItem = computed(() =>
    Math.min(this.page() * this.itemsPerPage, this.totalItems),
  );
}

With Ellipsis

Use getPaginationRange() from @avesra/angular to compute visible page numbers and insert span[av-pagination-ellipsis] between ranges.

import { Component, computed, model } from '@angular/core';
import {
  getPaginationRange,
  AvPaginationImports,
} from '@avesra/angular';

@Component({
  selector: 'app-pagination-with-ellipsis-demo',
  imports: [AvPaginationImports],
  template: `<div class="w-full max-w-2xs overflow-x-auto sm:max-w-full">
  <av-pagination class="justify-center">
    <ul av-pagination-content>
      <li av-pagination-item>
        <button
          av-pagination-prev
          [disabled]="page() === 1"
          (click)="page.set(page() - 1)"
        >
          <span av-pagination-prev-icon></span>
          <span>Previous</span>
        </button>
      </li>
      @for (item of range(); track $index) {
        <li av-pagination-item>
          @if (item === 'ellipsis') {
            <span av-pagination-ellipsis></span>
          } @else {
            <button
              av-pagination-link
              [active]="item === page()"
              (click)="page.set(item)"
            >
              {{ item }}
            </button>
          }
        </li>
      }
      <li av-pagination-item>
        <button
          av-pagination-next
          [disabled]="page() === totalPages"
          (click)="page.set(page() + 1)"
        >
          <span>Next</span>
          <span av-pagination-next-icon></span>
        </button>
      </li>
    </ul>
  </av-pagination>
</div>`,
})
export class PaginationWithEllipsisDemo {
  readonly page = model(1);
  readonly totalPages = 12;

  readonly range = computed(() => getPaginationRange(this.page(), this.totalPages));
}

With Summary

Add div[av-pagination-summary] alongside the content list to show result counts.

Showing 1-10 of 120 results
import { Component, computed, model } from '@angular/core';
import {
  getPaginationRange,
  AvPaginationImports,
} from '@avesra/angular';

@Component({
  selector: 'app-pagination-with-summary-demo',
  imports: [AvPaginationImports],
  template: `<av-pagination class="w-full">
  <div av-pagination-summary>
    Showing {{ startItem() }}-{{ endItem() }} of {{ totalItems }} results
  </div>
  <ul av-pagination-content>
    <li av-pagination-item>
      <button
        av-pagination-prev
        [disabled]="page() === 1"
        (click)="page.set(page() - 1)"
      >
        <span av-pagination-prev-icon></span>
        <span>Previous</span>
      </button>
    </li>
    @for (item of range(); track $index) {
      <li av-pagination-item>
        @if (item === 'ellipsis') {
          <span av-pagination-ellipsis></span>
        } @else {
          <button
            av-pagination-link
            [active]="item === page()"
            (click)="page.set(item)"
          >
            {{ item }}
          </button>
        }
      </li>
    }
    <li av-pagination-item>
      <button
        av-pagination-next
        [disabled]="page() === totalPages"
        (click)="page.set(page() + 1)"
      >
        <span>Next</span>
        <span av-pagination-next-icon></span>
      </button>
    </li>
  </ul>
</av-pagination>`,
})
export class PaginationWithSummaryDemo {
  readonly page = model(1);
  readonly totalPages = 12;
  readonly itemsPerPage = 10;
  readonly totalItems = 120;

  readonly range = computed(() => getPaginationRange(this.page(), this.totalPages));
  readonly startItem = computed(() => (this.page() - 1) * this.itemsPerPage + 1);
  readonly endItem = computed(() =>
    Math.min(this.page() * this.itemsPerPage, this.totalItems),
  );
}

Custom Icons

Replace the default chevrons by projecting custom content into span[av-pagination-prev-icon] and span[av-pagination-next-icon].

import { Component, computed, model } from '@angular/core';
import { AvPaginationImports } from '@avesra/angular';

@Component({
  selector: 'app-pagination-custom-icons-demo',
  imports: [AvPaginationImports],
  template: `<av-pagination class="justify-center">
  <ul av-pagination-content>
    <li av-pagination-item>
      <button
        av-pagination-prev
        [disabled]="page() === 1"
        (click)="page.set(page() - 1)"
      >
        <span av-pagination-prev-icon>
          <svg
            class="size-4"
            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"
            aria-hidden="true"
          >
            <line x1="19" y1="12" x2="5" y2="12" />
            <polyline points="12 19 5 12 12 5" />
          </svg>
        </span>
        <span>Back</span>
      </button>
    </li>
    @for (p of pages(); track p) {
      <li av-pagination-item>
        <button
          av-pagination-link
          [active]="p === page()"
          (click)="page.set(p)"
        >
          {{ p }}
        </button>
      </li>
    }
    <li av-pagination-item>
      <button
        av-pagination-next
        [disabled]="page() === totalPages"
        (click)="page.set(page() + 1)"
      >
        <span>Forward</span>
        <span av-pagination-next-icon>
          <svg
            class="size-4"
            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"
            aria-hidden="true"
          >
            <line x1="5" y1="12" x2="19" y2="12" />
            <polyline points="12 5 19 12 12 19" />
          </svg>
        </span>
      </button>
    </li>
  </ul>
</av-pagination>`,
})
export class PaginationCustomIconsDemo {
  readonly page = model(1);
  readonly totalPages = 3;

  readonly pages = computed(() =>
    Array.from({ length: this.totalPages }, (_, index) => index + 1),
  );
}

Styling

Passing Tailwind CSS classes

Pass utility classes on individual Pagination parts — for example av-pagination, ul[av-pagination-content], and button[av-pagination-link].

import { Component, computed, model } from '@angular/core';
import { AvPaginationImports } from '@avesra/angular';

@Component({
  selector: 'app-pagination-custom-styling-demo',
  imports: [AvPaginationImports],
  template: `<av-pagination class="gap-8 justify-center">
  <ul av-pagination-content class="gap-2">
    <li av-pagination-item>
      <button
        av-pagination-prev
        [disabled]="page() === 1"
        (click)="page.set(page() - 1)"
      >
        <span av-pagination-prev-icon></span>
        <span>Previous</span>
      </button>
    </li>
    @for (p of pages(); track p) {
      <li av-pagination-item>
        <button
          av-pagination-link
          class="rounded-md"
          [active]="p === page()"
          (click)="page.set(p)"
        >
          {{ p }}
        </button>
      </li>
    }
    <li av-pagination-item>
      <button
        av-pagination-next
        [disabled]="page() === totalPages"
        (click)="page.set(page() + 1)"
      >
        <span>Next</span>
        <span av-pagination-next-icon></span>
      </button>
    </li>
  </ul>
</av-pagination>`,
})
export class PaginationCustomStylingDemo {
  readonly page = model(1);
  readonly totalPages = 3;

  readonly pages = computed(() =>
    Array.from({ length: this.totalPages }, (_, index) => index + 1),
  );
}

Customizing the component classes

To customize the Pagination classes, use the @layer components directive. Learn more.

@layer components {
  .av-pagination {
    @apply gap-8;
  }

  .av-pagination__link {
    @apply rounded-md;
  }

  .av-pagination__summary {
    @apply text-xs font-semibold;
  }
}

Avesra follows the BEM methodology so component variants and states stay reusable and easy to customize.

CSS Classes

The Pagination component uses these CSS classes:

Base & Layout Classes

  • .av-pagination — Root navigation container with flex layout
  • .av-pagination__summary — Left-side info text container
  • .av-pagination__content — Container for pagination items
  • .av-pagination__item — Individual item wrapper
  • .av-pagination__link — Page number button
  • .av-pagination__link--nav — Navigation button modifier (Previous/Next)
  • .av-pagination__ellipsis — Ellipsis indicator

Size Classes

  • .av-pagination--sm — Small size variant
  • .av-pagination--md — Medium size variant (default)
  • .av-pagination--lg — Large size variant

Interactive States

The component supports both CSS pseudo-classes and data attributes:

  • Active page:[data-active="true"] or [aria-current="page"]
  • Hover::hover or [data-hovered="true"]
  • Focus::focus-visible or [data-focus-visible="true"]
  • Disabled::disabled or [aria-disabled="true"]
  • Pressed::active or [data-pressed="true"]

API Reference

Compound API across av-pagination, summary, content, item, link, prev/next, and ellipsis parts. Page changes use native (click) handlers. Use getPaginationRange() to build ellipsis-aware page lists.

PropTypeDefaultDescription
size'sm' | 'md' | 'lg''md'Size of the pagination items (av-pagination).
activebooleanfalseWhether this is the current page (button[av-pagination-link]).
disabledbooleanfalseWhether the control is disabled (button[av-pagination-link], button[av-pagination-prev], button[av-pagination-next]).

Accessibility

Pagination provides:

  • Semantic <nav> via role="navigation" with aria-label="pagination" on av-pagination
  • Active page indicated via aria-current="page" on button[av-pagination-link]
  • Keyboard navigation via Tab through all interactive elements
  • Focus ring on keyboard navigation via :focus-visible
  • Ellipsis marked with aria-hidden="true" to avoid screen reader confusion
  • Disabled states communicated via native disabled and aria-disabled

Made with ❤ by SyntaxHertz. Open source and available on GitHub.