Back
A tutorial on how to build a clean pagination component using text labels for better readability.
<nav class="text-pagination" aria-label="Page navigation">
<div class="page-controls">
<button class="page-link prev" id="prev-page" aria-label="Go to previous page">Previous</button>
<div class="page-numbers">
<button class="page-link number">1</button>
<button class="page-link number">2</button>
<button class="page-link number active">3</button>
<button class="page-link number">4</button>
<button class="page-link number">5</button>
</div>
<button class="page-link next" id="next-page" aria-label="Go to next page">Next</button>
</div>
<div class="page-info" id="page-info">Page 3 of 5</div>
</nav>
The key aspects of our HTML structure:
- We use a semantic nav element with an appropriate aria-label
- Text-based "Previous" and "Next" buttons instead of icon buttons
- Numbered page buttons in the middle
- An informative text showing the current page and total page count
- Clear class names that describe the purpose of each element
document.addEventListener('DOMContentLoaded', () => {
const prevButton = document.getElementById('prev-page');
const nextButton = document.getElementById('next-page');
const pageInfo = document.getElementById('page-info');
const pageButtons = document.querySelectorAll('.page-link.number');
// Set initial page
let currentPage = 3;
const totalPages = pageButtons.length;
// Update UI to reflect current page
function updatePageUI() {
// Update page buttons
pageButtons.forEach((button, index) => {
const pageNum = index + 1;
if (pageNum === currentPage) {
button.classList.add('active');
button.setAttribute('aria-current', 'page');
button.disabled = true;
} else {
button.classList.remove('active');
button.removeAttribute('aria-current');
button.disabled = false;
}
});
// Update page info text
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
// Enable/disable prev/next buttons
prevButton.disabled = currentPage === 1;
nextButton.disabled = currentPage === totalPages;
// Add/remove disabled class for styling
if (currentPage === 1) {
prevButton.classList.add('disabled');
} else {
prevButton.classList.remove('disabled');
}
if (currentPage === totalPages) {
nextButton.classList.add('disabled');
} else {
nextButton.classList.remove('disabled');
}
}
// Set page and update UI
function goToPage(pageNum) {
currentPage = pageNum;
updatePageUI();
// In a real app, you would load the content for the new page here
console.log(`Navigated to page ${currentPage}`);
}
// Add click handlers for prev/next buttons
prevButton.addEventListener('click', () => {
if (currentPage > 1) {
goToPage(currentPage - 1);
}
});
nextButton.addEventListener('click', () => {
if (currentPage < totalPages) {
goToPage(currentPage + 1);
}
});
// Add click handlers for page number buttons
pageButtons.forEach((button, index) => {
button.addEventListener('click', () => {
goToPage(index + 1);
});
});
// Initialize the UI
updatePageUI();
});
Our JavaScript includes these key features:
- A centralized function to update the UI state
- ARIA attributes for better accessibility
- Proper disabling of buttons when they're not usable
- Event handlers for all interactive elements
- Dynamic page information text
/* Main container */
.text-pagination {
--primary: #3b82f6;
--text: #374151;
--border: #e5e7eb;
--bg-hover: #f3f4f6;
--disabled: #9ca3af;
--active-bg: #3b82f6;
--active-text: white;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 600px;
margin: 0 auto;
}
/* Controls container */
.page-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
/* Page numbers container */
.page-numbers {
display: flex;
gap: 4px;
}
Now let's style the pagination buttons:
/* All pagination links */
.page-link {
background: transparent;
border: 1px solid var(--border);
padding: 8px 12px;
border-radius: 4px;
color: var(--text);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
/* Prev/Next buttons */
.page-link.prev,
.page-link.next {
padding: 8px 16px;
}
/* Number buttons */
.page-link.number {
min-width: 40px;
text-align: center;
}
/* Hover effects */
.page-link:hover:not(:disabled):not(.active) {
background-color: var(--bg-hover);
border-color: #d1d5db;
}
/* Active state */
.page-link.active {
background-color: var(--active-bg);
color: var(--active-text);
border-color: var(--active-bg);
}
/* Disabled state */
.page-link:disabled,
.page-link.disabled {
opacity: 0.6;
cursor: not-allowed;
}
Let's style the page info text and add focus states for accessibility:
/* Page info text */
.page-info {
text-align: center;
font-size: 14px;
color: var(--text);
margin-top: 8px;
}
/* Focus styles for accessibility */
.page-link:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
Finally, let's make our pagination responsive for mobile devices:
/* Responsive adjustments */
@media (max-width: 480px) {
.page-controls {
flex-direction: column;
gap: 16px;
}
.page-numbers {
order: 1;
}
.page-link.prev {
order: 2;
align-self: flex-start;
}
.page-link.next {
order: 3;
align-self: flex-end;
margin-top: -44px; /* Align with prev button */
}
}
This text-based pagination component offers several advantages:
- Improved clarity: Text labels like "Previous" and "Next" are more explicit than arrows
- Better accessibility: Text labels provide clear meaning for screen readers
- Informative context: The page information text helps users understand their location
- User-friendly design: Clean layout with proper spacing and visual states
- Mobile responsiveness: Adapts to different screen sizes with an optimized layout
- Smooth interactions: Subtle transitions and clear hover/active states
This pagination style is particularly well-suited for content-focused websites like blogs, articles, or documentation sites where clarity is more important than compact design.<nav class="text-pagination" aria-label="Page navigation">
<div class="page-controls">
<button class="page-link prev" id="prev-page" aria-label="Go to previous page">Previous</button>
<div class="page-numbers">
<button class="page-link number">1</button>
<button class="page-link number">2</button>
<button class="page-link number active">3</button>
<button class="page-link number">4</button>
<button class="page-link number">5</button>
</div>
<button class="page-link next" id="next-page" aria-label="Go to next page">Next</button>
</div>
<div class="page-info" id="page-info">Page 3 of 5</div>
</nav>
<script>
document.addEventListener('DOMContentLoaded', () => {
const prevButton = document.getElementById('prev-page');
const nextButton = document.getElementById('next-page');
const pageInfo = document.getElementById('page-info');
const pageButtons = document.querySelectorAll('.page-link.number');
// Set initial page
let currentPage = 3;
const totalPages = pageButtons.length;
// Update UI to reflect current page
function updatePageUI() {
// Update page buttons
pageButtons.forEach((button, index) => {
const pageNum = index + 1;
if (pageNum === currentPage) {
button.classList.add('active');
button.setAttribute('aria-current', 'page');
button.disabled = true;
} else {
button.classList.remove('active');
button.removeAttribute('aria-current');
button.disabled = false;
}
});
// Update page info text
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
// Enable/disable prev/next buttons
prevButton.disabled = currentPage === 1;
nextButton.disabled = currentPage === totalPages;
// Add/remove disabled class for styling
if (currentPage === 1) {
prevButton.classList.add('disabled');
} else {
prevButton.classList.remove('disabled');
}
if (currentPage === totalPages) {
nextButton.classList.add('disabled');
} else {
nextButton.classList.remove('disabled');
}
}
// Set page and update UI
function goToPage(pageNum) {
currentPage = pageNum;
updatePageUI();
// In a real app, you would load the content for the new page here
console.log(`Navigated to page ${currentPage}`);
}
// Add click handlers for prev/next buttons
prevButton.addEventListener('click', () => {
if (currentPage > 1) {
goToPage(currentPage - 1);
}
});
nextButton.addEventListener('click', () => {
if (currentPage < totalPages) {
goToPage(currentPage + 1);
}
});
// Add click handlers for page number buttons
pageButtons.forEach((button, index) => {
button.addEventListener('click', () => {
goToPage(index + 1);
});
});
// Initialize the UI
updatePageUI();
});
</script>
<style>
/* Main container */
.text-pagination {
--primary: #3b82f6;
--text: #374151;
--border: #e5e7eb;
--bg-hover: #f3f4f6;
--disabled: #9ca3af;
--active-bg: #3b82f6;
--active-text: white;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 600px;
margin: 0 auto;
}
/* Controls container */
.page-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
/* Page numbers container */
.page-numbers {
display: flex;
gap: 4px;
}
/* All pagination links */
.page-link {
background: transparent;
border: 1px solid var(--border);
padding: 8px 12px;
border-radius: 4px;
color: var(--text);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
/* Prev/Next buttons */
.page-link.prev,
.page-link.next {
padding: 8px 16px;
}
/* Number buttons */
.page-link.number {
min-width: 40px;
text-align: center;
}
/* Hover effects */
.page-link:hover:not(:disabled):not(.active) {
background-color: var(--bg-hover);
border-color: #d1d5db;
}
/* Active state */
.page-link.active {
background-color: var(--active-bg);
color: var(--active-text);
border-color: var(--active-bg);
}
/* Disabled state */
.page-link:disabled,
.page-link.disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Page info text */
.page-info {
text-align: center;
font-size: 14px;
color: var(--text);
margin-top: 8px;
}
/* Focus styles for accessibility */
.page-link:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
/* Responsive adjustments */
@media (max-width: 480px) {
.page-controls {
flex-direction: column;
gap: 16px;
}
.page-numbers {
order: 1;
}
.page-link.prev {
order: 2;
align-self: flex-start;
}
.page-link.next {
order: 3;
align-self: flex-end;
margin-top: -44px; /* Align with prev button */
}
}
</style>
Thank you for reading this article.
Comments