NETFRAME_DOCS // PAGINATION
GITHUB

Pagination

Page navigation rendered as a <nav> + <ol>. Controls wrap onto another line when space is tight, without shrinking the touch targets.

These working examples paginate a local sample dataset: selecting a page changes the current-page state, record list and announced status. Previous/Next are disabled at the boundaries. For server navigation, use anchors with real page URLs instead of the local-demo buttons. The CSS library does not fetch or paginate data for you.

Basic

BASIC PAGINATION

Page 1 of 5

  • NODE_001
  • NODE_002
  • NODE_003
<div data-pagination data-pages="5" data-page="1">
  <nav aria-label="Basic pagination">
    <ol class="nf-pagination">
    <li class="nf-page-item nf-page-item-disabled"><button type="button" class="nf-page-link nf-page-prev" data-page="0" data-action="prev" disabled aria-disabled="true">PREV</button></li>
    <li class="nf-page-item nf-page-item-active"><button type="button" class="nf-page-link" data-page="1" data-action="page" aria-current="page">1</button></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link" data-page="2" data-action="page">2</button></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link" data-page="3" data-action="page">3</button></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link" data-page="4" data-action="page">4</button></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link" data-page="5" data-action="page">5</button></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link nf-page-next" data-page="2" data-action="next">NEXT</button></li>
    </ol>
  </nav>
  <p data-page-status role="status">Page 1 of 5</p>
  <ul data-page-results aria-label="Records on this page">
    <li>NODE_001</li>
    <li>NODE_002</li>
    <li>NODE_003</li>
  </ul>
</div>

With ellipsis

TRUNCATED RANGE

Page 8 of 24

  • NODE_022
  • NODE_023
  • NODE_024
<div data-pagination data-pages="24" data-page="8">
  <nav aria-label="Truncated range">
    <ol class="nf-pagination">
    <li class="nf-page-item"><button type="button" class="nf-page-link nf-page-prev" data-page="7" data-action="prev">PREV</button></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link" data-page="1" data-action="page">1</button></li>
    <li class="nf-page-item nf-page-ellipsis"><span class="nf-page-link" aria-hidden="true">…</span></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link" data-page="7" data-action="page">7</button></li>
    <li class="nf-page-item nf-page-item-active"><button type="button" class="nf-page-link" data-page="8" data-action="page" aria-current="page">8</button></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link" data-page="9" data-action="page">9</button></li>
    <li class="nf-page-item nf-page-ellipsis"><span class="nf-page-link" aria-hidden="true">…</span></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link" data-page="24" data-action="page">24</button></li>
    <li class="nf-page-item"><button type="button" class="nf-page-link nf-page-next" data-page="9" data-action="next">NEXT</button></li>
    </ol>
  </nav>
  <p data-page-status role="status">Page 8 of 24</p>
  <ul data-page-results aria-label="Records on this page">
    <li>NODE_022</li>
    <li>NODE_023</li>
    <li>NODE_024</li>
  </ul>
</div>

JavaScript

The optional controller below runs both examples. Copy it alongside the HTML, then replace the sample records array with your own data. The ellipsis range updates as you move through the pages.

document.querySelectorAll('[data-pagination]').forEach((demo) => {
  const pages = Number(demo.dataset.pages);
  let current = Number(demo.dataset.page);
  const list = demo.querySelector('.nf-pagination');
  const status = demo.querySelector('[data-page-status]');
  const results = demo.querySelector('[data-page-results]');
  const records = Array.from({ length: pages * 3 }, (_, i) => 'NODE_' + String(i + 1).padStart(3, '0'));

  function render(focusAction) {
    const numbers = pages <= 7
      ? Array.from({ length: pages }, (_, i) => i + 1)
      : [1, ...[current - 1, current, current + 1].filter((p) => p > 1 && p < pages), pages];
    list.replaceChildren();
    function control(label, page, action) {
      const item = document.createElement('li');
      item.className = 'nf-page-item';
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'nf-page-link' + (action === 'page' ? '' : ' nf-page-' + action);
      button.textContent = label;
      button.dataset.page = String(page);
      button.dataset.action = action;
      if (action === 'page' && page === current) {
        item.classList.add('nf-page-item-active');
        button.setAttribute('aria-current', 'page');
      }
      if (page < 1 || page > pages) {
        item.classList.add('nf-page-item-disabled');
        button.disabled = true;
        button.setAttribute('aria-disabled', 'true');
      }
      item.append(button);
      list.append(item);
    }
    control('PREV', current - 1, 'prev');
    numbers.forEach((page, i) => {
      if (i && page - numbers[i - 1] > 1) {
        const item = document.createElement('li');
        item.className = 'nf-page-item nf-page-ellipsis';
        const dots = document.createElement('span');
        dots.className = 'nf-page-link';
        dots.textContent = '…';
        dots.setAttribute('aria-hidden', 'true');
        item.append(dots);
        list.append(item);
      }
      control(String(page), page, 'page');
    });
    control('NEXT', current + 1, 'next');
    demo.dataset.page = String(current);
    status.textContent = 'Page ' + current + ' of ' + pages;
    results.replaceChildren(...records.slice((current - 1) * 3, current * 3).map((record) => {
      const item = document.createElement('li');
      item.textContent = record;
      return item;
    }));
    if (focusAction) {
      const candidate = list.querySelector('[data-action="' + focusAction + '"]:not(:disabled)');
      (focusAction === 'page' ? list.querySelector('[aria-current="page"]') : candidate || list.querySelector('[aria-current="page"]')).focus();
    }
  }

  demo.addEventListener('click', (event) => {
    const button = event.target.closest('button[data-page]');
    if (!button || button.disabled) return;
    const page = Number(button.dataset.page);
    if (page < 1 || page > pages || page === current) return;
    current = page;
    render(button.dataset.action);
  });
  render();
});

Class reference

Class Description
nf-pagination <ol> wrapper — wrapping flex row with a small gap
nf-page-item <li> wrapper
nf-page-item-active Fills the current page control with primary colour
nf-page-item-disabled Dims controls; also set disabled on buttons or aria-disabled on links
nf-page-ellipsis Non-interactive spacer item
nf-page-link Anchor, button or span inside each item
nf-page-prev Previous label styling
nf-page-next Next label styling