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.

Immediately above, though. --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

PartValue
Rule1 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 lineGeist Mono, 11 px, uppercase, --text-secondary
Slot44 × 44 px minimum, 2 px corner, tabular figures
Slot at restno contour, no fill — --text-secondary type only
Slot on hover or focus1 px --border-strong contour, type to --text-primary, 120 ms
Current slotopaque black plate, white label, not a link
Stepsame slot with a 16 px chevron and a word; chevron leans 3 px on hover or focus, 240 ms
Gapan ellipsis, no box, no target, aria-hidden
Slot gutter--space-1 (4 px)
Maximum slotsseven, counting both ellipses
Why the current page is a plate and not lime. Lime is light, not a surface, and there is one lime moment per screen — on a listing page that moment belongs to the call to action, not to a numeral. The plate is the move .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
Why there is no contour at rest. Contour before fill governs how an object is drawn; at rest a page number is type, not an object. The contour arriving on hover is also the only thing that can show you the target is 44 px when the numeral is 11 px — which is well past the 24 px floor WCAG 2.2 SC 2.5.8 sets, and the reason the slots do not shrink on mobile.

Behaviour

Accessibility

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 --flush when 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.