Components
Pagination
The closing bracket of a listing. It is the section header read backwards — rule on
top, mono counter left, controls right — so an archive is framed by one shape rather
than by two unrelated ones. Links and a <span>, no script.
Default — a page in the middle
Both steps present, both ellipses present, seven slots at most. This is the shape you see on nine of eleven pages.
The two ends
At either end the step that has nowhere to go is removed, not disabled. A disabled control is a target that answers nothing when you reach it; an absent one never asks. The row stays put because the status line holds the left edge.
Steps only
Drop the numbered slots when the set has no fixed end — a feed, a search that keeps loading, anything where “von 11” would be a guess. The status line goes with them, because a count you cannot state is not a status.
Under a ruled container
A blog grid, a table, anything whose own bottom border already draws a full-width
hairline: add --flush and the pagination drops its border-top.
One rule does both jobs. Without it the two hairlines stack 0 px apart and that edge
reads 2 px while every other edge on the page reads 1 px — the same contract
.cf-section-header--flush has, turned the other way up.
--flush defers to an adjacent
rule. Put anything between the two — patterns/news.html hangs the blog
timeline axis under its grid — and the rule it was deferring to is no longer adjacent,
so the pagination ends up with no rule at all and floats. That page therefore keeps its
own border-top, and the rule closes the axis band instead. If something sits
in between, don't use the modifier.
Markup
<nav class="cf-pagination cf-pagination--flush" aria-label="Beiträge, Seiten"> <p class="cf-pagination__status">Seite 6 von 11</p> <ul class="cf-pagination__list" role="list"> <li> <a class="cf-pagination__step cf-pagination__step--prev" href="?seite=5" rel="prev"> <svg class="cf-icon cf-icon--sm" aria-hidden="true"><use href="#cf-chevron-left"></use></svg> Zurück </a> </li> <li><a class="cf-pagination__page" href="?seite=1" aria-label="Seite 1">1</a></li> <li><span class="cf-pagination__gap" aria-hidden="true">…</span></li> <li><a class="cf-pagination__page" href="?seite=5" aria-label="Seite 5">5</a></li> <!-- the current page is a span, never a link --> <li><span class="cf-pagination__page" aria-current="page">6</span></li> <li><a class="cf-pagination__page" href="?seite=7" aria-label="Seite 7">7</a></li> <li><span class="cf-pagination__gap" aria-hidden="true">…</span></li> <li><a class="cf-pagination__page" href="?seite=11" aria-label="Seite 11, letzte Seite">11</a></li> <li> <a class="cf-pagination__step cf-pagination__step--next" href="?seite=7" rel="next"> Weiter <svg class="cf-icon cf-icon--sm" aria-hidden="true"><use href="#cf-chevron-right"></use></svg> </a> </li> </ul> </nav>
The chevrons come from the drawn icon set, so the page needs
assets/js/cf-icons.js — or the sprite pasted inline. They are the two glyphs
the set was drawn for: cf-chevron-left is documented as “Previous, back” and
cf-chevron-right as “Next, forward”. The filled CF arrow is not used here; it
belongs to buttons and to links that go somewhere new, and a page of the same listing is
not somewhere new. → Iconography
Anatomy
| Part | Value |
|---|---|
| Rule | 1 px solid on top, full width — or none with --flush |
| Air above the rule | --space-8 (32 px), the component's own margin — 0 with --flush |
| Air under the rule | --space-4 (16 px) |
| Status line | Geist Mono, 11 px, uppercase, --text-secondary |
| Slot | 44 × 44 px minimum, 2 px corner, tabular figures |
| Slot at rest | no contour, no fill — --text-secondary type only |
| Slot on hover or focus | 1 px --border-strong contour, type to --text-primary, 120 ms |
| Current slot | opaque black plate, white label, not a link |
| Step | same slot with a 16 px chevron and a word; chevron leans 3 px on hover or focus, 240 ms |
| Gap | an ellipsis, no box, no target, aria-hidden |
| Slot gutter | --space-1 (4 px) |
| Maximum slots | seven, counting both ellipses |
.cf-btn--solid already makes for “this is the one”, and it clears contrast
by a distance nothing else in the row needs to be measured against: white on black,
21:1. → Colour
Behaviour
- Below 34 rem of container every numbered slot except the current one is hidden, along with both ellipses. Zurück and Weiter carry the component and the status line says where you are. The threshold is a container query, so the same component folds correctly inside a column as it does on a full-width page.
-
The steps at the ends are removed, not disabled. Page 1 has no Zurück;
the last page has no Weiter. Nothing shifts, because the status line anchors the left
edge and the list is pushed right by
margin-inline-start: auto. -
No script. Each slot is a real link to a real URL —
?seite=6or/news/seite-6, the server decides. The page reloads and the reader keeps a back button, a bookmarkable address and a working middle click. -
Nothing animates but the chevron. Contour and colour cross-fade in
120 ms, the chevron leans 3 px in 240 ms. Under
prefers-reduced-motionevery duration token drops to 1 ms, so all three become instant without a rule of their own. → Motion
Accessibility
- A
<nav>with anaria-labelthat names what is paged — “Beiträge, Seiten”, not “Navigation”, which the role already says. - The slots are an
<ul>, so the count is announced on entry.role="list"is on it becauselist-style: noneremoves the role in Safari. aria-current="page"on the current slot, and the current slot is a<span>— a link to the page you are on is a target that does nothing.- Each numbered link carries
aria-label="Seite N": a bare “7” in a link list says nothing about what it is 7 of. The last one says “Seite 11, letzte Seite”. - The ellipsis is
aria-hidden. What it stands for is stated properly by the status line, which is the first thing inside the labelled nav and is therefore read on entry. - Targets are 44 × 44 px, past SC 2.5.8's 24 px minimum with the gutter on top.
- Contrast:
--text-secondaryon the page wash is 5.9:1, the hover contour is full ink, and the current plate is 21:1. The focus ring is the system's, unchanged — and:focus-visiblenow also draws the contour and inks the type, so tabbing through the slots reads the same as pointing at them. The current slot takes neither: it is a<span>, and every rule here is prefixeda.. - Under forced colours the current plate falls back to
Highlight/HighlightText— the pair the mode reserves for the selected one of a set.
rel="prev" and rel="next" stay on the steps.
What Google dropped in 2019 was <link rel="next"> in the head as an
indexing signal, and that is not what these are. On an <a> the link
type is still valid HTML and still describes the relationship for anything that reads
it. It is a semantic hint, not an SEO mechanism, and nothing on the page depends on it.
Rules
Do
- Put it under the listing it pages, with
--flushwhen that listing is ruled. - Keep it to seven slots. First, last, current and one neighbour each side is the whole job.
- Give the nav a label that names the collection, so a page with two of them stays unambiguous.
- Drop the numbers and the status line when the set has no known end.
- Let the server paginate. The URL is the state.
Don't
- Don't link the current page, and don't disable the steps at the ends — remove them.
- Don't paginate under about four screens of content. Show the lot.
- Don't fill the current slot with lime. Lime is the page's one light moment and it is spent elsewhere.
- Don't add a jump-to-page field. Eleven pages of a blog is not a database.
- Don't let it wrap onto a third line — that is what the 34 rem fold is for.