Pagination

@use 'sprucecss/scss/spruce' as *;

@include generate-btn(
  '.pagination__page-number',
  null,
  false,
  true
);

.pagination {
  display: flex;
  justify-content: center;

  + .pagination {
    margin-block-start: spacer('l');
  }

  &__links {
    @include clear-list;
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: spacer('xs');
    justify-content: center;

    > li {
      margin-block: 0;
      padding-block: 0;
    }
  }

  &__page-number {
    @include btn-variant-soft('primary');
    font-variant-numeric: tabular-nums;
    justify-content: center;
    min-inline-size: 2.75rem;

    &[aria-current='page'] {
      background-color: color('primary-background', 'btn');
      border-color: color('primary-background', 'btn');
      color: color('primary-foreground', 'btn');
    }

    &[aria-disabled='true'] {
      opacity: 0.45;
      pointer-events: none;
    }

    svg {
      block-size: 1em;
      inline-size: 1em;
    }
  }

  &__ellipsis {
    color: color('muted');
    padding-inline: spacer('xxs');
  }

  // On phones, the previous and next links show only their arrows.
  &__direction-text {
    @include breakpoint-down('sm') {
      @include visually-hidden;
    }
  }

  &--sm &__page-number {
    font-size: config('font-size', $btn-sm);
    min-inline-size: 2.25rem;
    padding: config('padding', $btn-sm);
  }
}

[dir='rtl'] .pagination__direction svg {
  scale: -1 1;
}
<nav class="pagination" aria-label="Posts pagination">
    <ul class="pagination__links">
        <li><span class="pagination__page-number pagination__direction" aria-disabled="true"><svg aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m15 18-6-6 6-6"/></svg><span class="pagination__direction-text">Previous<span class="sr-only"> page</span></span></span></li>
        <li><a class="pagination__page-number" href="#" aria-current="page"><span class="sr-only">Page </span>1</a></li>
        <li><a class="pagination__page-number" href="#"><span class="sr-only">Page </span>2</a></li>
        <li><a class="pagination__page-number" href="#"><span class="sr-only">Page </span>3</a></li>
        <li><a class="pagination__page-number" href="#"><span class="sr-only">Page </span>4</a></li>
        <li><span class="pagination__ellipsis" aria-hidden="true">…</span></li>
        <li><a class="pagination__page-number" href="#"><span class="sr-only">Page </span>10</a></li>
        <li><a class="pagination__page-number pagination__direction" href="#" rel="next"><span class="pagination__direction-text">Next<span class="sr-only"> page</span></span><svg aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m9 18 6-6-6-6"/></svg></a></li>
    </ul>
</nav>
<nav class="pagination pagination--sm" aria-label="Comments pagination">
    <ul class="pagination__links">
        <li><a class="pagination__page-number pagination__direction" href="#" rel="prev"><svg aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m15 18-6-6 6-6"/></svg><span class="pagination__direction-text">Previous<span class="sr-only"> page</span></span></a></li>
        <li><a class="pagination__page-number" href="#"><span class="sr-only">Page </span>1</a></li>
        <li><span class="pagination__ellipsis" aria-hidden="true">…</span></li>
        <li><a class="pagination__page-number" href="#"><span class="sr-only">Page </span>4</a></li>
        <li><a class="pagination__page-number" href="#" aria-current="page"><span class="sr-only">Page </span>5</a></li>
        <li><a class="pagination__page-number" href="#"><span class="sr-only">Page </span>6</a></li>
        <li><span class="pagination__ellipsis" aria-hidden="true">…</span></li>
        <li><a class="pagination__page-number" href="#"><span class="sr-only">Page </span>12</a></li>
        <li><a class="pagination__page-number pagination__direction" href="#" rel="next"><span class="pagination__direction-text">Next<span class="sr-only"> page</span></span><svg aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m9 18 6-6-6-6"/></svg></a></li>
    </ul>
</nav>

Pagination

Page links for long lists: previous and next, the first and last pages, the pages around the current one and ellipses for the gaps.

Variations

  • default: buttons with the soft primary style; the current page is solid.
  • .pagination--sm: smaller buttons, for comments or tables.

Anatomy

  • .pagination is a nav with a specific label ("Posts pagination"), so it's distinguishable from other navigations.
  • .pagination__links is the list; every page is a .pagination__page-number link (generated with the generate-btn mixin, so it's sized like a button).
  • .pagination__direction are the previous and next links, .pagination__ellipsis the gaps.

Accessibility

  • Every page link reads "Page 3" (with a visually hidden "Page"), and the current one has aria-current="page".
  • The previous and next links keep their text ("Previous page") for screen readers even on phones, where only the arrows are visible.
  • On the first page, "Previous" isn't a link but a span with aria-disabled="true": there's nowhere to go, so it shouldn't be in the tab order. The ellipses are hidden from screen readers.
  • rel="prev" and rel="next" describe the relation of the pages.
  • The links are at least 2.75rem wide, which is easy to tap.

Technical Details

  • The colors come from Spruce's btn-variant-soft() mixin and the primary button tokens, so the custom pagination button colors aren't needed any more, and it follows the dark mode and every style.
  • Show the first, the last and one or two pages around the current one; more makes the control hard to scan.
  • The arrows flip in RTL.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.