
Pure CSS Pagination with :target
:target rule doing the switching — plus a :has() fallback that keeps page one visible when nothing is targeted at all.Published
20 hand-coded CSS pagination designs for admin dashboards, e-commerce product grids, blog archives, search results, and documentation. Covers pure-CSS :target pagination with no JavaScript, ellipsis truncation for large page ranges, responsive layouts that collapse to a compact page-X-of-N on mobile, jump-to-page and items-per-page controls, Tailwind and Bootstrap 5 variants, and a WCAG reference build with aria-current. 16 are pure CSS. Scoped under .pgn-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Related39 CSS Breadcrumbs32 CSS Tab Designs15 CSS Navigation Menu Designs51 CSS Buttons

:target rule doing the switching — plus a :has() fallback that keeps page one visible when nothing is targeted at all.Published

Published

Previous · Page 3 of 12 · Next when its own wrapper gets narrow. Because it queries the container and not the screen, the same component behaves correctly in a sidebar, a modal and a full-width page — drag the demo's resize handle to watch it switch.Published
disabled attribute rather than a class, hit targets are a full 44 px, and every icon-only control carries a real accessible name.Published

min/max outside a form validate nothing), and reports both the error and the new record range to assistive tech.Published

<select> is restyled without losing its real menu or keyboard type-ahead.Published

@apply-style component classes that stop twelve number buttons from each carrying forty utilities. No CDN and no build step: the demo reproduces those utilities as scoped vanilla CSS so it renders identically in an isolated frame.Published

--bs-pagination-* custom properties, so restyling it no longer means a specificity war or a Sass rebuild. This keeps the canonical .pagination / .page-item / .page-link structure and its .active and .disabled states, and rewrites only the variables — neutral surface, softer radius, a near-black active fill instead of the default blue.Published

<nav> landmark, an ordered list, aria-labels that read as sentences ("Page 5, current page"), a 2 px :focus-visible ring with offset, a redundant non-color cue on the active page, and a forced-colors block so the state survives Windows high contrast mode.Published

:has() rule hide every row whose data-page does not match — so arrow-key traversal comes free and the state survives a repaint.Published

Published

Published

Published

translate with a little overshoot.Published

:hover and :focus-visible so a keyboard user gets identical feedback. Nothing animated touches layout.Published

Published

Published

Published

backdrop-filter.Published

Published
:target pseudo-class matches an element whose id equals the current URL fragment, so linking to #page-3 makes that panel match :target and you show it while hiding the rest. The whole mechanic is: hide every panel, then #page-1:target, #page-2:target { display: block }, plus a :not(:target) default so page one shows on first load when there is no fragment yet. The active state on the number itself comes from .panel:target ~ nav a[href="#page-3"] or, more cleanly, a :has() selector on the container. Two honest limits. This is client-side only — the URL fragment is never sent to the server, so it cannot paginate content that has not been loaded, which rules it out for a 10,000-row table. And it changes the browser history, so Back steps through pages, which is usually what people want but occasionally is not. Where it shines is documentation tabs, FAQ sections, image galleries and any paginated block whose total content is small enough to ship at once. Demo 01 is the reference.1 … 7 8 [9] 10 11 … 214. Keep the number of rendered items constant as the user moves; if the window shrinks near the edges the whole bar changes width and the numbers shift under the cursor, which causes mis-clicks. The fix is to expand the window when you are near either end so the total count stays the same: at page 2 you render 1 [2] 3 4 5 6 … 214 rather than a short bar. On the markup, an ellipsis is not a page, so it must not be a link or a button — use a <span aria-hidden="true"> so keyboard users tab from page to page without landing on a dead stop, and screen readers do not announce a meaningless character. Demo 02 implements the stable-window version.<nav aria-label="Pagination"> — the label matters because most pages have several navigation landmarks and "navigation" alone tells a screen-reader user nothing. Put the pages in a list so the count is announced ("list, 9 items"), which gives blind users the same at-a-glance sense of scale that sighted users get. Mark the active page with aria-current="page" — that is the semantically correct value here, not aria-current="true", and style off the attribute rather than a separate class so the visual and the semantic state can never drift apart. Finally, make the numbers real <a href> elements when they navigate, or real <button> elements when they update in place; a <div onclick> is invisible to keyboards. Two extras worth doing: give each link an accessible name beyond the bare digit (aria-label="Page 3") so it is unambiguous in a links list, and include a visually-hidden live region announcing "Showing 21-40 of 214" after a page change. Demo 09 is the reference implementation.@container (max-width: 30rem) responds to the space the pagination actually has, so the same component works in a narrow sidebar on desktop and in a full-width mobile layout without special cases. Hide the numbers with display: none rather than visibility: hidden so they leave the tab order too. If you want a middle tier, an intermediate breakpoint can keep a three-number window with the ellipsis removed. Demo 03 shows the full-to-compact transition.?page=7 can be linked, bookmarked and crawled, and users can tell how much is left. Cursor-based load-more wins for chronological feeds where new items arrive constantly: offset pagination with a moving dataset shows duplicates and skips items, because inserting a row at the top shifts everything down and page two now starts one row later than it did a second ago. A cursor keyed to the last item's id or timestamp is immune to that. Avoid true infinite scroll on anything with a footer, on anything users need to leave and return to, and on anything you want indexed — Google renders pages but does not scroll them, so content only reachable by scrolling may never be crawled. The practical middle ground, and the pattern most e-commerce sites converge on, is a "Load more" button that also updates the URL, giving you both the shallow interaction and the addressable state. Demo 13 covers the cursor pattern.rel="next" and rel="prev" as an indexing signal in 2019 — they are harmless to keep for other consumers but they do nothing for Google now. What matters instead: every paginated page must be reachable by a real crawlable <a href> link, so pagination driven purely by JavaScript click handlers with no href can leave pages two onward undiscovered. Each page should self-canonicalize to its own URL, not to page one — canonicalizing every page to the first is a common and costly mistake that tells Google the other pages do not deserve indexing. Do not create a "view all" page and canonicalize to it unless that page genuinely loads fast. Keep the URL pattern boring and stable (/blog/page/3/ or ?page=3), and make sure page one is available at the bare URL rather than at ?page=1, so you do not split signals across two addresses for the same content. Finally, paginated pages need genuinely distinct titles or Search Console will flag duplicates.[aria-current="page"] directly — then setting the attribute is the single source of truth and there is no way to end up with a highlighted link that screen readers do not announce as current. For prev and next at the ends of the range, the control should be genuinely inert rather than just faded: if it is a <button>, use the disabled attribute; if it is an <a>, remove the href and add aria-disabled="true", because a link without href is not focusable and cannot be activated, whereas a greyed-out link with a live href is a trap. Never rely on colour alone for either state — the active page should differ in weight, background or a visible indicator, not just hue, since a red-versus-grey distinction is invisible to a significant share of users. Contrast applies to the disabled state too: WCAG exempts disabled controls from the 4.5:1 requirement, but a control nobody can read is still a bad control, so aim for 3:1 anyway..pgn-NN rules, and they behave identically in any framework. In component terms the natural interface is currentPage, totalPages and an onPageChange callback, with the ellipsis window computed in a small pure function you can unit-test separately from the rendering. That function is the only genuinely tricky part of pagination and it is worth extracting. For the four demos with JavaScript, wire them into the usual hooks: useEffect with a returned cleanup in React, onMounted plus onBeforeUnmount in Vue, onMount returning cleanup in Svelte. In the Next.js App Router, prefer driving pagination through searchParams so the server component re-renders with the right slice and the URL stays shareable — that keeps pages crawlable, which a purely client-side implementation does not. Demo 07 is already written as Tailwind utilities and demo 08 as Bootstrap 5 classes, both scoped so they cannot leak into a host page. Each design is roughly 1-3KB of CSS.32 free CSS accordions covering pure-CSS FAQ blocks with details/summary, mobile navigation menus, e-commerce filter sidebars, nested tree views, exclusive single-open groups and smoothly animated height:auto — plus 26 visual variations in vertical, horizontal and hybrid layouts. Copy-paste HTML and CSS, no JavaScript.
39 hand-coded CSS breadcrumbs — Schema.org BreadcrumbList Microdata + WCAG 2.2 accessible (Google Rich Results eligible), :has() responsive collapse, dark/light theme toggle with color-mix, Shopify/Amazon e-commerce filter chip removal, Vercel/Linear/Notion SaaS dashboard app-shell pattern, Docusaurus/Mintlify sticky scroll-shrink header, Amazon-style dropdown sibling menus, NFT/Web3 holographic shimmer (@property + conic-gradient), SaaS marketing gradient text, Airbnb filter chip with container queries, editorial blog wave underline, developer docs one-click path copy (Clipboard API), and 26 more. Semantic HTML: <nav aria-label='Breadcrumb'> + <ol> + aria-current='page'. Framework-agnostic, MIT-licensed.
26 hand-coded CSS circular and radial menu designs, from the floating-action FAB speed dial and dashboard radial hubs to game-style pie-slice wheels, SVG gooey metaball fans, hexagonal honeycombs, iris-aperture reveals, orbiting satellites on offset-path, compass-rose directional nav, sci-fi HUD rings, glassmorphism popovers built on the native Popover API, vinyl-record players, and full-takeover nebula overlays on native dialog. Every design uses CSS trigonometry (sin(), cos()) or the counter-rotation trick for positioning, ships real keyboard navigation, aria-labels on icon-only buttons, and a prefers-reduced-motion guard. Copy-paste HTML, CSS and vanilla JS with zero framework dependencies — MIT licensed.