Most CSS pagination starts with a nav element full of anchors, and the stylesheet handles layout, link states, spacing and small-screen behavior. You see it on blog archives, search results and product listings, anywhere content gets split across pages.
A compact bar of page numbers can fail an accessibility review on size alone. WCAG 2.2 criterion 2.5.8 sets a minimum of 24 by 24 CSS pixels at Level AA (W3C, Understanding SC 2.5.8, updated May 2026), and a one-digit link with no padding comes in under that.
Search engines don’t care about any of the styling. They read the links underneath, and that’s it.
Which HTML markup does CSS pagination need?
See the Pen
Clip-path Pagination by Aashima Jain (@Aashima)
on CodePen.
The whole structure is a nav element with aria-label=”Pagination” wrapped around an unordered list. Every page number gets its own list item and an anchor with a real href.
The nav landmark lets screen reader users jump straight to the control, and the list tells them how many items they’re moving through.
<nav aria-label="Pagination">
<ul class="pagination">
<li><a href="/blog/?page=1">Previous</a></li>
<li><a href="/blog/?page=1">1</a></li>
<li><a href="/blog/?page=2" aria-current="page">2</a></li>
<li><a href="/blog/?page=3">3</a></li>
<li><a href="/blog/?page=3">Next</a></li>
</ul>
</nav>A typical pagination UI has previous and next links (text labels, since arrow glyphs alone tell a screen reader very little), numbered anchors with real URLs, one link carrying aria-current=”page”, and an ellipsis item that is plain text standing in for skipped pages.
MDN’s pagination recipe (last updated November 2025) follows this skeleton. It adds visually hidden text before each number and puts aria-hidden on the arrow glyphs.
The widely copied W3Schools how-to goes a shorter way: a div, bare anchors with href=”#” and an .active class on the current page. It looks fine. A screen reader hears an unlabeled run of links and has no idea which page is current.
Browsers add bullets to a ul by default. Set the list-style property to none and zero out the margin and padding while you’re at it.
Layout methods for pagination links: flexbox, grid and inline-block
See the Pen
Roundie Pagination by ahmed beheiry (@ahmedbeheiry)
on CodePen.
Flexbox is the one to reach for. Put display: flex on the ul to get a row, and on the nav to center that row.
MDN’s pagination recipe nests them the same way, with spacing set through either a margin on the items or a gap on the list.
nav { display: flex; justify-content: center; }
.pagination { display: flex; gap: 4px; list-style: none; margin: 0; padding: 0; }| Method | Key properties | Centering | Weak point |
|---|---|---|---|
| Flexbox | display: flex, gap, flex-wrap | justify-content: center | Equal-width cells need extra rules |
| Grid | display: grid, grid-auto-flow: column, gap | justify-content: center | More setup for a changing link count |
| Inline-block or float | display: inline-block, float: left | text-align: center (inline-block only) | Whitespace gaps, clearing, no float centering |
Grid starts to make sense when every cell has to be the same width. If you want to try track values first, a CSS grid generator saves some trial and error before anything goes into the stylesheet.
The float: left pattern from the W3Schools example still renders fine. You can’t center floats, though, and the container needs a clearing rule.
I’d go with flexbox on anything new and only touch floats when a stylesheet already depends on them.
How to style hover, focus, current and disabled states
See the Pen
Tooltip Pagination by Joe (@dope)
on CodePen.
Give every state its own selector and keep them separate. The base .pagination a rule carries padding, a border, the text color and a short transition. .pagination a:hover gets a background shift and nothing else. .pagination a:focus-visible gets a clear outline with an offset, and .pagination a[aria-current="page"] gets a filled background with high-contrast text.
Disabled items need different treatment, which the first subsection below covers.
Hover is where people go overboard. W3C’s guidance for SC 1.4.11 says extra hover treatments need no contrast ratio of their own, as long as the control keeps its contrast while hovered.
Wrap hover rules in @media (hover: hover) so touch screens don’t keep a stuck hover after a tap.
For more color and motion ideas, a collection of CSS hover effects is worth browsing. A plain background shift is still the safer pick for page links.
:focus-visible shows the ring when the browser decides focus should be visible, which in practice means keyboard use. MDN lists it as widely available since March 2022.
Take the outline away with nothing in its place and keyboard users lose their spot on the page. MDN says as much.
Current page and disabled links
See the Pen
pagination indicators by thykka (@thykka)
on CodePen.
Drive the current-page style from [aria-current="page"], the way MDN’s recipe does with a dark background and white text. One attribute then controls both the look and what assistive technology announces.
A hand-written .active class drifts over time. Someone moves the class and forgets the attribute, or the other way around.
On page 1 the Previous item has nowhere to go. Render it as a span with muted text, so it’s neither a link nor focusable, or drop it from the list on the first and last pages.
Avoid pointer-events: none on a live link. It blocks the mouse and leaves the link reachable by keyboard.
WCAG exempts inactive controls from its contrast rules (SC 1.4.3 and 1.4.11, W3C), so a muted disabled item is allowed. Keep it legible anyway.
Rounded and bordered buttons
See the Pen
CSS3 – Neumorphism Pagination Design by Claudio Rigollet (@claudiorigo)
on CodePen.
MDN’s recipe uses a 1px border and a 0.2em radius on each link. That’s about as plain as a bordered bar gets, and it works.
Round number buttons are popular. They also fail the target size test more often than square ones.
W3C’s Understanding document for SC 2.5.8 points out that rounded corners can leave no room for a 24 by 24 pixel square inside the target. A circle needs a diameter of about 34 CSS pixels (the diagonal of that square) to contain one, unless the spacing exception applies.
Use a border radius generator to preview the shape, then check the finished size.
Making pagination responsive on small screens
See the Pen
Pagination Buttons by Sibusiso Mtombeni (@sibusiso-mtombeni)
on CodePen.
Below a breakpoint, hide the middle page numbers and keep previous, next, first, last and the current page. Nine numbers in a row rarely fit a phone width.
The numbers further from the current page go, and an ellipsis item takes their place. It should be text, not a link.
CSS can’t count outward from the current page. So the server tags the numbers far from it with a class (.far in the example below), and the stylesheet decides whether those tagged items show.
.pagination .far { display: none; }
@media (min-width: 640px) {
.pagination .far { display: block; }
}The reveal rule sits inside a media query set at the width where the full bar fits.
Write the compact bar as the base style and widen it with min-width queries. That’s the mobile-first approach.
A pure :nth-child() rule can hide a fixed middle block, for example li:nth-child(n+4):nth-last-child(n+4). It also hides the current page whenever that page lands in the middle, so treat it as a prototype trick and keep it out of production.
Don’t shrink padding to cram more numbers in. Squeezed links are exactly how responsive bars end up under the 24 pixel target minimum.
How do you make CSS pagination accessible?
Most of it comes down to markup and a few lines of CSS. Mark the current link with aria-current=”page”, label the nav, keep a visible keyboard focus ring, give every link at least 24 by 24 CSS pixels (or enough spacing around it), and hold the contrast ratios.
Size comes first. Level AA sets the minimum target size at 24 x 24 CSS px, SC 2.5.8 (W3C, WCAG 2.2). There’s also a spacing exception built on a 24 px circle, so 20 px targets with a 4 px gap pass and 20 px targets with no gap fail (W3C, Understanding SC 2.5.8, updated 11 May 2026).
Contrast is the other half. Text needs 4.5:1, or 3:1 when it’s large (W3C, SC 1.4.3), and controls plus their focus or selected states need 3:1 to be identifiable (W3C, SC 1.4.11).
Native elements cover most of the semantics, and ARIA fills the state gap. The attribute aria-current=”page” tells assistive technology which link matches the highlighted page.
Set it on one link only. MDN notes that an unrecognized value is read as true, so a typo still announces “current”.
The reverse fails too. A highlighted link with no attribute announces nothing, and an attribute with no visible style helps screen reader users only.
MDN’s pagination recipe adds a visually hidden “page” before each number and hides arrow glyphs with aria-hidden. A bare “2” and a bare chevron tell a screen reader very little.
The focus ring is a component state, so the 3:1 rule applies to it. W3C’s own failing example is a yellow ring on a white background.
Text in every state needs 4.5:1 against its background, including the filled current-page link. Check the color contrast of each state instead of eyeballing it.
A one-digit link is narrow, and padding does the work. W3C’s sufficient technique C42 sets min-height and min-width on the target container.
The spacing exception is a fallback, not a plan. W3C recommends meeting the size minimum regardless of spacing, and points to the stricter AAA criterion 2.5.5 Target Size (Enhanced) for important controls.
Bootstrap, Tailwind or custom CSS for pagination
See the Pen
Pagination One by Steven Roberts (@matchboxhero)
on CodePen.
Reuse the framework already on the site. Write custom CSS only when a framework would otherwise be added for this one component.
Bootstrap ships the markup contract already: a nav, a ul.pagination, li.page-item and a.page-link, with .active and .disabled sitting on the item. Tailwind has no pagination class at all, so every link carries its own utility classes. With custom CSS you own every selector and breakpoint, and the accessibility checks that come with them.
Bootstrap’s documentation suggests labeling the nav by purpose, such as “Search results pages” for a results list. That beats a generic label when a page carries more than one nav.
Bootstrap added pagination CSS variables in version 5.2.0, and the 5.3 documentation exposes them for the active and disabled colors, so theming rarely needs overrides.
Bootstrap’s documentation places aria-current=”page” on the active li.page-item in its 5.0, 5.2 and 5.3 releases alike. MDN places it on the link itself.
Disabled items differ too. The 5.0 documentation keeps a dead link (href="#") and adds tabindex="-1" plus aria-disabled="true". The 5.2 and 5.3 documentation drops the href from disabled links, but still advises tabindex="-1" and custom JavaScript, because pointer-events: none does not stop keyboard use. It also allows swapping the anchor for a span, which needs none of that.
Tailwind ships utilities, not components. Pagination arrives as pre-built blocks such as the Tailwind Plus set (page numbers, next and previous buttons, ellipses), or as hand-built markup like the examples in this collection of Tailwind pagination patterns.
If you’re still choosing a framework for the whole project, the Tailwind vs Bootstrap comparison matters more than this one component.
Pure CSS pagination without JavaScript: :target, radio hack and scroll snap
See the Pen
Pure CSS pagination by Brendan Mullins (@jsnanigans)
on CodePen.
You can switch visible content with no script through the :target pseudo-class, through hidden radio inputs read with :checked, or through scroll snap. All of them keep every page inside one document.
:target works by giving each panel an id, pointing a link at it with a fragment, and letting the CSS show only the matching panel. MDN notes the browser may jump the scroll position to the target. The radio hack uses hidden inputs and labels, with a sibling selector showing one panel at a time. Scroll snap locks panels into place through scroll-snap-type and scroll-snap-align, and there’s no page-number state to style.
Without a fragment in the URL, no panel matches :target. The first panel needs its own fallback rule.
The upside is that there’s no script to load or fail, and switching is instant since nothing gets fetched. That suits a small, fixed set of panels.
The downsides pile up quickly. Every page ships in the first response, which cancels the faster initial load and lower network traffic that Google lists as benefits of loading content in increments. Fragments give no unique URL, so all panels share one address. The radio hack leaves no shareable state, and screen readers meet radio inputs where they expect links.
MDN documents :target as fragment-driven styling. Google’s pagination documentation (last updated 10 December 2025) says fragment identifiers are ignored for page numbers, so these panels cannot rank as separate pages.
Where CSS-only pagination stops working
It’s fine for a small gallery. A blog archive, a search results page or a product listing is a different story, because each page needs its own URL in the index, the list grows past what one response should carry, and readers want to share or return to a position.
When any of that applies, move to server-rendered pages with real URLs, or add a thin layer of JavaScript that fetches each page.
What makes paginated pages crawlable?
Plain anchor links pointing to a separate URL for each page. Google’s crawlers read the href of anchor (a) elements and do not click buttons or run functions that wait for a user action (Google Search Central, updated 10 December 2025).
Give each page a unique URL, such as a ?page=n parameter, because Google treats URLs in a sequence as separate pages. Each page should canonicalize to itself, not to page one.
Skip fragments. Google ignores the text after a # for page numbers and may skip the link as already fetched. Every page should also link to the next one, and optionally back to the first page.
Titles and descriptions do not have to differ across the sequence, since Google tries to recognize the pages in a series. Filtered or re-sorted variants are a separate case, and those should be blocked with noindex or robots.txt.
Google Webmasters announced on March 21, 2019 that it had retired rel=”next” and rel=”prev” as an indexing signal, and Search Engine Journal reported it had stopped using them years earlier. Google’s current documentation adds that other search engines may still read the links.
SEO sources still disagree on what to do. NoGood’s 2024 guide lists the tags among best practices, while Ahrefs says to leave an existing implementation alone.
Both positions hold up. Google ignores the tags and other engines may not, so removing existing ones gains nothing. Adding them to a new build buys you nothing in Google, though.
The common failure is a Next button wired to a click handler with no URL behind it. The page looks paginated to a reader and shows up as one page to the crawler.
Pagination, load more or infinite scroll: which should you use?
See the Pen
Pagination, pager by Elmira Mukhamedjanova (@ElmiraMukhamedjanova)
on CodePen.
Use numbered pagination for content people need to find again or share. Use load more for browse-heavy lists, and infinite scroll only when every chunk has its own URL.
| Pattern | Crawlable by default | Reader sees | Build cost |
|---|---|---|---|
| Numbered pagination | Yes, with anchor links | Result size and position | Low |
| Load more | Only with page URLs underneath | A growing list and one button | Medium |
| Infinite scroll | Only with page URLs and URL updates | An endless list, unclear size | Highest |
Google lists the trade-offs plainly. Pagination shows result size and position but splits content across page loads, while infinite scroll can cause scrolling fatigue because the total is unclear.
Baymard’s usability research (a 2016 write-up of its year-long studies of more than 50 leading e-commerce sites, published through Smashing Magazine) found testers called pagination slow. They also browsed less of the product list than on load more or infinite scroll sites.
So the two sources pull in opposite directions. Google counts visible position as a strength of pagination, while Baymard’s testers treated a row of numbered links as friction.
The difference is the lens. Google describes what each pattern shows and what a crawler can index, and Baymard watched people browse.
The study is also a decade old, so treat it as a pointer and not a current benchmark.
Baymard’s own example is Crutchfield. It loads 20 products by default, lazy-loads 20 more as the reader nears the 10th, and shows a Load more button after the 40th.
A load more button usually fetches the next batch through Ajax and appends it to the same page. Google’s crawlers do not click the button, so the batches need page URLs underneath.
Google’s documentation is specific about what makes infinite scroll indexable.
- Each chunk gets a persistent, unique URL, and the same content loads on that URL every time
- Page numbers are absolute, like
?page=12, never relative like?date=yesterday - Chunks link to each other in sequence
- The address bar updates through the History API as each chunk becomes the main visible element
Default to numbered pagination. Move to load more over real ?page=n URLs when breadth of browsing matters more than reaching a specific page, and skip infinite scroll unless you’ll build the URL layer.
CSS Pagination Examples To Check Out
Follow The Line by Mark Mead
See the Pen
CSS Line Follow Pagination by Mark Mead (@markmead)
on CodePen.
Customize Me! by Simon Goellner
See the Pen
Pagination by Simon Goellner (@simeydotme)
on CodePen.
Dance of the Numbers
See the Pen
Pagination with morphing numbers by Mikael Ainalem (@ainalem)
on CodePen.
Modern Vibes by Milica
See the Pen
responsive pagination by Milica (@micadev)
on CodePen.
Step Up The Game by Brandon Capp
See the Pen
Animated Pagination Dots by Brandon Capp (@brandoncapp)
on CodePen.
Pure Essence by Muhammad Rauf
See the Pen
Pagination using HTML and CSS by Muhammad Rauf (@softcodeon)
on CodePen.
Jump n’ Bounce!
See the Pen
Bouncy Pagination by Adam Kuhn (@cobra_winfrey)
on CodePen.
Chomp-Chomp by Paraskevas Dinakis
See the Pen
Pac-Man CSS Pagination by Paraskevas Dinakis (@perry_nt)
on CodePen.
Elegance in Motion
See the Pen
Beautifully animated Pagination style in Pure css by Aashima Jain (@Aashima)
on CodePen.
Splash of Color by Mamun Khandaker
See the Pen
Creative stylish pagination design by Mamun Khandaker (@kh-mamun)
on CodePen.
Glide & Shine
See the Pen
CSS Pagination with Hover Effect by freefrontend.com (@cssparadise)
on CodePen.
Six Flavors by Béla Varga
See the Pen
Pure CSS3 Responsive Pagination by Béla Varga (@netzzwerg)
on CodePen.
Jump the Dots with Elliot Geno
See the Pen
Dot Hopper – Pagination by Elliot Geno (@pyrografix)
on CodePen.
Wichert’s Wonderland
See the Pen
#dailyui 085 Pagination by Andre Wichert (@andrewichert)
on CodePen.
Compact Control by Wayne Dunkley
See the Pen
Dynamic Page Controls by Wayne Dunkley (@waynedunkley)
on CodePen.
Bles’s Brainchild
See the Pen
WebDesignerDepot pagination by Wouter Bles (@wouterbles)
on CodePen.
Stephanie’s Stellar Table
See the Pen
Materialize + Angular Search and Filter Responsive Table with pagination by Stephanie (@StuffieStephie)
on CodePen.
Space-Saver by Mariusz Dabrowski
See the Pen
Infinite Pagination by Mariusz Dabrowski (@MarioD)
on CodePen.
Center Stage by Vamsikrishna Kodimela
See the Pen
Simple CSS Pagination by Vamsikrishna Kodimela (@vamsikrishna-kodimela)
on CodePen.
Keep It Simple with Gerardo Valencia
See the Pen
Simple pagination CSS by Gerardo Valencia (@grardovr)
on CodePen.
Magic Guideline by Ryan Yu
See the Pen
Responsive Magic Line Pagination by Ryan Yu (@iamryanyu)
on CodePen.
Munchy Moves with Mikael Ainalem
See the Pen
Pacman pagination by Mikael Ainalem (@ainalem)
on CodePen.
Pedro Rago’s Slick Line
See the Pen
Line Pagination Navbar by Pedro Rago (@PedroRago10)
on CodePen.
Michael’s Classic Roman Spin
See the Pen
Roman Pagination by Michael Sveistrup (@faffelkugel)
on CodePen.
Himalaya’s Handy Navigation
See the Pen
Pagination or Navigation Links Application by Himalaya Singh (@himalayasingh)
on CodePen.
Kasper’s Geometric Groove
See the Pen
Pagination by Kasper Mikiewicz (@Idered)
on CodePen.
Hakim’s Arrow Acrobatics
See the Pen
Flexing pagination arrows by Hakim El Hattab (@hakimel)
on CodePen.
Darin’s Yeti Wave
See the Pen
Yeti Hand Pagination by Darin (@dsenneff)
on CodePen.
Rohenha’s Animation Alchemy
See the Pen
Pagination animation experiment by Rohenha (@rohenha)
on CodePen.
CSS Pagination FAQ
Is CSS pagination the same as CSS paged media?
No. CSS paged media covers print and fragmented output, where rules such as @page and break properties decide how content splits across physical pages. Pagination as a screen component styles clickable page links and has no connection to print layout.
Should the current page be a link or a span?
Either works. A link keeps markup and target size consistent across the bar, while a span avoids a pointless self-link. Keep aria-current=”page” on whichever element you use, since in MDN’s breadcrumb example the attribute is optional only when the item is not a link.
How many page links should show at once?
See the Pen
pagination hover animation by Elena Nazarova (@nazarelen)
on CodePen.
Fewer than most designs show. Display the current page, one or two neighbors on each side, the first and last pages, and an ellipsis between gaps. Baymard’s testers were discouraged by more than a handful of links.
Can buttons replace links in pagination?
Not for page navigation. Google’s crawlers read the href of anchor elements and do not click buttons, so a button with no URL behind it hides pages two onward. Buttons suit load more controls, and only when page URLs exist underneath.
Testing Pagination for Keyboard, Screen Reader and Crawl Problems
CSS pagination passes testing when every page link is a real anchor with its own URL, because that single choice covers keyboard users and screen readers as well as Googlebot.
Start with the crawl. Turn JavaScript off and confirm each page number loads a distinct URL. Crawl failures stay invisible to visitors, so they go first.
Then tab through the bar and confirm every link shows a visible focus ring. That takes about a minute. The screen reader pass comes last because it takes longest: check that the landmark name and the current page both get announced.
The trade-off is one server request per page, which script-driven buttons avoid.
The web accessibility checklist extends these checks to the rest of the page.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026


