Пагинация

Компонент постраничной навигации — кнопки «назад/вперёд», номера страниц, многоточие при большом числе страниц.

Задача

Нужно отобразить навигацию по страницам списка с учётом текущей страницы. При большом количестве страниц — показывать только часть номеров с многоточием.

Решение

Логика генерации страниц:

// utils/pagination.js

function getPages(current, total, delta = 2) {
  const range = ;
  const rangeWithDots = ;
  let prev;

  for (let i = 1; i <= total; i++) {
    if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) {
      range.push(i);
    }
  }

  for (const i of range) {
    if (prev !== undefined) {
      if (i - prev === 2) {
        rangeWithDots.push(prev + 1); // одна пропущенная — показываем цифру, не "..."
      } else if (i - prev > 2) {
        rangeWithDots.push('...');
      }
    }
    rangeWithDots.push(i);
    prev = i;
  }

  return rangeWithDots;
}

HTML + CSS:

<nav class="pagination" aria-label="Пагинация" id="pagination"></nav>
.pagination {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
}

.pagination__btn {
  min-width: 36px;
  height: 36px;
  padding: 0 8px;
  border: 1px solid #e2e8f0;
  border-radius: 6px;
  background: #fff;
  cursor: pointer;
  font-size: 0.875rem;
  color: #475569;
  transition: background 0.15s, color 0.15s;
}

.pagination__btn:hover:not(:disabled) { background: #f1f5f9; }
.pagination__btn.active { background: #3b82f6; color: #fff; border-color: #3b82f6; }
.pagination__btn:disabled { opacity: 0.4; cursor: not-allowed; }
.pagination__dots { padding: 0 4px; color: #94a3b8; }
import { getPages } from './utils/pagination.js';

let currentPage = 1;
const totalPages = 20;

function renderPagination(current, total) {
  const container = document.getElementById('pagination');
  const pages = getPages(current, total);

  container.innerHTML = '';

  // Кнопка "назад"
  const prev = createBtn('‹', current === 1, () => changePage(current - 1));
  prev.setAttribute('aria-label', 'Предыдущая страница');
  container.appendChild(prev);

  // Страницы и многоточия
  pages.forEach((page) => {
    if (page === '...') {
      const span = document.createElement('span');
      span.className = 'pagination__dots';
      span.textContent = '…';
      container.appendChild(span);
    } else {
      const btn = createBtn(page, false, () => changePage(page));
      if (page === current) btn.classList.add('active');
      btn.setAttribute('aria-current', page === current ? 'page' : undefined);
      container.appendChild(btn);
    }
  });

  // Кнопка "вперёд"
  const next = createBtn('›', current === total, () => changePage(current + 1));
  next.setAttribute('aria-label', 'Следующая страница');
  container.appendChild(next);
}

function createBtn(label, disabled, onClick) {
  const btn = document.createElement('button');
  btn.className = 'pagination__btn';
  btn.textContent = label;
  btn.disabled = disabled;
  btn.addEventListener('click', onClick);
  return btn;
}

function changePage(page) {
  currentPage = page;
  renderPagination(currentPage, totalPages);
  // loadData(currentPage);
}

renderPagination(currentPage, totalPages);

Ключевые моменты

  • getPages генерирует массив с номерами и '...' — вся логика отделена от рендера.
  • aria-current="page" на текущей кнопке — доступность для screen reader.
  • disabled на крайних кнопках — нельзя уйти за пределы диапазона.
  • URL должен отражать текущую страницу: ?page=3 — для шаринга ссылки и возврата по истории.

Связанные рецепты / темы