Back
Learn how to build a compact pagination component that handles large numbers of pages using ellipsis.
<nav class="pagination" aria-label="Page navigation">
<button class="pagination-item" aria-label="Go to previous page" id="prev-button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<div class="pagination-list" id="pagination-list">
<button class="pagination-item">1</button>
<button class="pagination-item">2</button>
<button class="pagination-item active">3</button>
<span class="pagination-ellipsis">…</span>
<button class="pagination-item">8</button>
<button class="pagination-item">9</button>
<button class="pagination-item">10</button>
</div>
<button class="pagination-item" aria-label="Go to next page" id="next-button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</nav>
Key elements of our HTML structure:
- We use a semantic nav element with appropriate ARIA labels for accessibility
- Previous and next buttons with clear SVG icons
- A container for the page numbers and ellipsis
- The ellipsis is represented by the HTML entity …
- The current page has an "active" class
document.addEventListener('DOMContentLoaded', () => {
const prevButton = document.getElementById('prev-button');
const nextButton = document.getElementById('next-button');
const paginationList = document.getElementById('pagination-list');
// Configuration
const totalPages = 10;
let currentPage = 3;
function updatePagination() {
// Clear current pagination list
paginationList.innerHTML = '';
// Helper function to create a page button
function createPageButton(pageNum, isActive = false) {
const button = document.createElement('button');
button.className = `pagination-item${isActive ? ' active' : ''}`;
button.textContent = pageNum;
button.addEventListener('click', () => setCurrentPage(pageNum));
button.disabled = isActive;
return button;
}
// Helper function to create an ellipsis
function createEllipsis() {
const span = document.createElement('span');
span.className = 'pagination-ellipsis';
span.innerHTML = '…';
return span;
}
// Always show first page
paginationList.appendChild(createPageButton(1, currentPage === 1));
// Logic for showing correct pagination items with ellipsis
if (currentPage > 3) {
paginationList.appendChild(createEllipsis());
}
// Show pages around current page
const startPage = Math.max(2, currentPage - 1);
const endPage = Math.min(totalPages - 1, currentPage + 1);
for (let i = startPage; i <= endPage; i++) {
if (i <= totalPages - 1 && i >= 2) {
paginationList.appendChild(createPageButton(i, currentPage === i));
}
}
// Add ellipsis before last page if needed
if (currentPage < totalPages - 2) {
paginationList.appendChild(createEllipsis());
}
// Always show last page
if (totalPages > 1) {
paginationList.appendChild(createPageButton(totalPages, currentPage === totalPages));
}
// Update prev/next button states
prevButton.disabled = currentPage === 1;
nextButton.disabled = currentPage === totalPages;
}
// Function to set current page and update UI
function setCurrentPage(pageNum) {
currentPage = pageNum;
updatePagination();
// In a real application, you would fetch data for the new page here
console.log(`Navigated to page ${currentPage}`);
}
// Add event listeners to prev/next buttons
prevButton.addEventListener('click', () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
});
nextButton.addEventListener('click', () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
});
// Initialize pagination
updatePagination();
});
The key features of our JavaScript implementation:
- Dynamically generates pagination based on current page position
- Always shows the first and last page
- Shows a small window of pages around the current page
- Uses ellipsis to indicate skipped pages when there are many pages
- Handles navigation with previous and next buttons
- Updates the UI whenever the current page changes
.pagination {
--text-color: #374151;
--background: white;
--border-color: #e5e7eb;
--hover-bg: #f9fafb;
--active-bg: #4f46e5;
--active-text: white;
--disabled-color: #d1d5db;
display: flex;
align-items: center;
gap: 6px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 8px 12px;
border-radius: 8px;
background: var(--background);
}
.pagination-list {
display: flex;
align-items: center;
gap: 4px;
}
.pagination-item {
display: flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
padding: 0 8px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--background);
color: var(--text-color);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
Let's style the different states of our pagination items:
.pagination-item:hover:not(:disabled):not(.active) {
background: var(--hover-bg);
border-color: #d1d5db;
}
.pagination-item.active {
background: var(--active-bg);
color: var(--active-text);
border-color: var(--active-bg);
cursor: default;
}
.pagination-item:focus-visible {
outline: 2px solid var(--active-bg);
outline-offset: 2px;
}
.pagination-item:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pagination-ellipsis {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
font-size: 18px;
color: var(--text-color);
user-select: none;
}
Finally, let's make our pagination responsive for smaller screens:
/* Responsive styles */
@media (max-width: 480px) {
.pagination {
padding: 6px 8px;
}
.pagination-item {
min-width: 32px;
height: 32px;
font-size: 13px;
}
.pagination-ellipsis {
width: 24px;
}
}
This pagination component with ellipsis offers several benefits:
- Scalability: Works well with any number of pages, from a few to hundreds
- Space efficiency: The ellipsis keeps the pagination compact even with many pages
- Context awareness: Always shows the user where they are in relation to the first and last page
- Accessibility: Uses semantic HTML and proper ARIA attributes for screen readers
- Visual clarity: Clear visual hierarchy with distinct active state and hover effects
- Adaptability: Responsive design works across different screen sizes
This pattern is ideal for search results, large product catalogs, document libraries, or any application with paginated content that might span many pages.
<nav class="pagination" aria-label="Page navigation">
<button class="pagination-item" aria-label="Go to previous page" id="prev-button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<div class="pagination-list" id="pagination-list">
<button class="pagination-item">1</button>
<button class="pagination-item">2</button>
<button class="pagination-item active">3</button>
<span class="pagination-ellipsis">…</span>
<button class="pagination-item">8</button>
<button class="pagination-item">9</button>
<button class="pagination-item">10</button>
</div>
<button class="pagination-item" aria-label="Go to next page" id="next-button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</nav>
<script>
document.addEventListener('DOMContentLoaded', () => {
const prevButton = document.getElementById('prev-button');
const nextButton = document.getElementById('next-button');
const paginationList = document.getElementById('pagination-list');
// Configuration
const totalPages = 10;
let currentPage = 3;
function updatePagination() {
// Clear current pagination list
paginationList.innerHTML = '';
// Helper function to create a page button
function createPageButton(pageNum, isActive = false) {
const button = document.createElement('button');
button.className = `pagination-item${isActive ? ' active' : ''}`;
button.textContent = pageNum;
button.addEventListener('click', () => setCurrentPage(pageNum));
button.disabled = isActive;
return button;
}
// Helper function to create an ellipsis
function createEllipsis() {
const span = document.createElement('span');
span.className = 'pagination-ellipsis';
span.innerHTML = '…';
return span;
}
// Always show first page
paginationList.appendChild(createPageButton(1, currentPage === 1));
// Logic for showing correct pagination items with ellipsis
if (currentPage > 3) {
paginationList.appendChild(createEllipsis());
}
// Show pages around current page
const startPage = Math.max(2, currentPage - 1);
const endPage = Math.min(totalPages - 1, currentPage + 1);
for (let i = startPage; i <= endPage; i++) {
if (i <= totalPages - 1 && i >= 2) {
paginationList.appendChild(createPageButton(i, currentPage === i));
}
}
// Add ellipsis before last page if needed
if (currentPage < totalPages - 2) {
paginationList.appendChild(createEllipsis());
}
// Always show last page
if (totalPages > 1) {
paginationList.appendChild(createPageButton(totalPages, currentPage === totalPages));
}
// Update prev/next button states
prevButton.disabled = currentPage === 1;
nextButton.disabled = currentPage === totalPages;
}
// Function to set current page and update UI
function setCurrentPage(pageNum) {
currentPage = pageNum;
updatePagination();
// In a real application, you would fetch data for the new page here
console.log(`Navigated to page ${currentPage}`);
}
// Add event listeners to prev/next buttons
prevButton.addEventListener('click', () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
});
nextButton.addEventListener('click', () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
});
// Initialize pagination
updatePagination();
});
</script>
<style>
.pagination {
--text-color: #374151;
--background: white;
--border-color: #e5e7eb;
--hover-bg: #f9fafb;
--active-bg: #4f46e5;
--active-text: white;
--disabled-color: #d1d5db;
display: flex;
align-items: center;
gap: 6px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 8px 12px;
border-radius: 8px;
background: var(--background);
}
.pagination-list {
display: flex;
align-items: center;
gap: 4px;
}
.pagination-item {
display: flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
padding: 0 8px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--background);
color: var(--text-color);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.pagination-item:hover:not(:disabled):not(.active) {
background: var(--hover-bg);
border-color: #d1d5db;
}
.pagination-item.active {
background: var(--active-bg);
color: var(--active-text);
border-color: var(--active-bg);
cursor: default;
}
.pagination-item:focus-visible {
outline: 2px solid var(--active-bg);
outline-offset: 2px;
}
.pagination-item:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pagination-ellipsis {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
font-size: 18px;
color: var(--text-color);
user-select: none;
}
/* Responsive styles */
@media (max-width: 480px) {
.pagination {
padding: 6px 8px;
}
.pagination-item {
min-width: 32px;
height: 32px;
font-size: 13px;
}
.pagination-ellipsis {
width: 24px;
}
}
</style>
Thank you for reading this article.
Comments
No comments yet. Be the first to comment!