Пагинация
Компонент постраничной навигации — кнопки «назад/вперёд», номера страниц, многоточие при большом числе страниц.
Задача
Нужно отобразить навигацию по страницам списка с учётом текущей страницы. При большом количестве страниц — показывать только часть номеров с многоточием.
Решение
Логика генерации страниц:
// 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— для шаринга ссылки и возврата по истории.