Patterns
Pagination
Pagination
Page navigation patterns for multi-page content, tables, and search results.
Overview
Pagination helps users navigate through large sets of content split across multiple pages. These patterns use semantic <nav> elements with proper ARIA attributes for accessibility.
Key features:
Semantic HTML with aria-label for screen readers
Current page indication via aria-current="page"
Disabled states for first/last page boundaries
Responsive design that works on all screen sizes
Keyboard navigation support
Simple Prev/Next
A minimal pagination with just Previous and Next buttons. Best for sequential content like blog posts or articles where page numbers are less important.
<nav class="pagination" aria-label="Pagination">
<ul>
<li>
<a href="#" data-prev aria-label="Previous page">
<icon-wc name="chevron-left" size="sm"></icon-wc>
Previous
</a>
</li>
<li>
<a href="#" data-next aria-label="Next page">
Next
<icon-wc name="chevron-right" size="sm"></icon-wc>
</a>
</li>
</ul>
</nav>
With Page Numbers
Pagination with numbered page links for direct page access. Includes ellipsis for large page ranges. Best for search results, product listings, or any content where users may want to jump to specific pages.
<nav class="pagination" aria-label="Pagination">
<ul>
<li>
<a href="#" data-prev aria-label="Previous page">
<icon-wc name="chevron-left" size="sm"></icon-wc>
</a>
</li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#" aria-current="page">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li><span data-ellipsis>...</span></li>
<li><a href="#">10</a></li>
<li>
<a href="#" data-next aria-label="Next page">
<icon-wc name="chevron-right" size="sm"></icon-wc>
</a>
</li>
</ul>
</nav>
Full Pagination
Complete pagination bar with result count, page navigation, and page size selector. Best for data tables and admin interfaces where users need full control over data display.
<div class="pagination-bar">
<p class="pagination-info" aria-live="polite">
Showing <strong>21</strong> to <strong>30</strong> of <strong>97</strong> results
</p>
<nav class="pagination" aria-label="Pagination">
<ul>
<li>
<a href="#" data-prev aria-label="First page">
<icon-wc name="chevrons-left" size="sm"></icon-wc>
</a>
</li>
<li>
<a href="#" data-prev aria-label="Previous page">
<icon-wc name="chevron-left" size="sm"></icon-wc>
</a>
</li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#" aria-current="page">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li><span data-ellipsis>...</span></li>
<li><a href="#">10</a></li>
<li>
<a href="#" data-next aria-label="Next page">
<icon-wc name="chevron-right" size="sm"></icon-wc>
</a>
</li>
<li>
<a href="#" data-next aria-label="Last page">
<icon-wc name="chevrons-right" size="sm"></icon-wc>
</a>
</li>
</ul>
</nav>
<div class="page-size-selector">
<label for="page-size">Show:</label>
<select id="page-size" aria-label="Items per page">
<option value="10">10</option>
<option value="25" selected>25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span aria-hidden="true">per page</span>
</div>
</div>
Disabled States
When on the first or last page, disable the corresponding navigation button to indicate the boundary. Use a <button> element with the disabled attribute instead of a link.
<!-- First page - Previous disabled -->
<nav class="pagination" aria-label="Pagination">
<ul>
<li>
<button type="button" disabled aria-label="Previous page">
<icon-wc name="chevron-left" size="sm"></icon-wc>
</button>
</li>
<li><a href="#" aria-current="page">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><span data-ellipsis>...</span></li>
<li><a href="#">10</a></li>
<li>
<a href="#" data-next aria-label="Next page">
<icon-wc name="chevron-right" size="sm"></icon-wc>
</a>
</li>
</ul>
</nav>
<!-- Last page - Next disabled -->
<nav class="pagination" aria-label="Pagination">
<ul>
<li>
<a href="#" data-prev aria-label="Previous page">
<icon-wc name="chevron-left" size="sm"></icon-wc>
</a>
</li>
<li><a href="#">1</a></li>
<li><span data-ellipsis>...</span></li>
<li><a href="#">8</a></li>
<li><a href="#">9</a></li>
<li><a href="#" aria-current="page">10</a></li>
<li>
<button type="button" disabled aria-label="Next page">
<icon-wc name="chevron-right" size="sm"></icon-wc>
</button>
</li>
</ul>
</nav>
CSS Styles
The base pagination styles are included in the nav element styles. These provide consistent sizing, hover states, and current page highlighting:
/* Pagination navigation - included in nav styles */
nav.pagination > ul {
display: flex;
align-items: center;
justify-content: center;
gap: var(--size-2xs);
}
nav.pagination li {
display: flex;
}
nav.pagination a,
nav.pagination button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
padding-inline: var(--size-xs);
font-size: var(--font-size-s);
font-weight: var(--font-weight-medium);
color: var(--color-text-muted);
background: transparent;
border: var(--border-width-thin) solid transparent;
border-radius: var(--radius-m);
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
nav.pagination a:hover:not([disabled]):not([aria-current]),
nav.pagination button:hover:not([disabled]):not([aria-current]) {
background: var(--color-gray-100);
color: var(--color-text);
}
nav.pagination [aria-current="page"] {
background: var(--color-interactive);
color: white;
border-color: var(--color-interactive);
}
nav.pagination [disabled] {
opacity: 0.5;
cursor: not-allowed;
}
/* Ellipsis */
nav.pagination [data-ellipsis] {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
color: var(--color-text-muted);
pointer-events: none;
}
Pagination Bar Styles
The pagination bar styles for the full variant (info text, nav, and page size selector) are included in VB's stylesheet. The layout uses CSS Grid to keep the nav centered while info and selector fill the remaining space. A container query handles responsive stacking:
/* Pagination bar layout — included in VB nav styles */
.pagination-bar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: var(--size-m);
padding: var(--size-m);
background: var(--color-surface-raised);
border-radius: var(--radius-m);
}
.pagination-info {
font-size: var(--font-size-s);
color: var(--color-text-muted);
}
.page-size-selector {
display: flex;
align-items: center;
justify-self: end;
gap: var(--size-xs);
font-size: var(--font-size-s);
color: var(--color-text-muted);
}
/* Responsive stacking via container query */
@container (max-width: 500px) {
.pagination-bar {
grid-template-columns: 1fr;
justify-items: center;
text-align: center;
}
.pagination-bar nav.pagination {
order: -1;
}
.pagination-bar .page-size-selector {
justify-self: center;
}
}
Usage Notes
Accessibility
Always wrap pagination in a <nav> element with aria-label="Pagination"
Mark the current page with aria-current="page"
Add aria-label to icon-only buttons (e.g., "Previous page", "Next page")
Use the disabled attribute on buttons for first/last page states
Add aria-live="polite" to the results count (.pagination-info) so screen readers announce updates when the page changes
Add aria-label="Items per page" on the page-size <select> for a complete accessible name
Ensure all interactive elements are keyboard accessible
When to Use Each Variant
Simple: Blog posts, articles, tutorials - sequential content where page numbers matter less
Numbered: Search results, product catalogs - when users need to jump to specific pages
Full: Data tables, admin dashboards - when users need result counts and page size control
Implementation Tips
For server-rendered pages, use <a> elements with proper URLs
For SPAs, use <button> elements with click handlers
Consider hiding page numbers on mobile and showing only prev/next
For very large datasets, consider infinite scroll as an alternative