Skip to content

Pagination

Edit on GitHub
@volvo-cars/css v2.3.0@volvo-cars/react-headless v0.24.7

Pagination helps users navigate through multiple pages, slides, or items. Choose the right pattern based on your use case:

Navigate through paginated data with numbered buttons and prev/next arrows. Use usePagination to generate the page list.

volvocars.com
import { type PaginationItem, usePagination } from '@volvo-cars/react-headless';
import { Icon, IconButton } from '@volvo-cars/react-icons';
export function PagePagination() {
const { setPage, pages, previousButtonProps, nextButtonProps } =
usePagination({ totalPages: 10, initialPage: 5 });
return (
<nav aria-label="pagination" className="flex-row gap-8">
<ul style={{ display: 'contents', listStyle: 'none' }}>
<li>
<IconButton
{...previousButtonProps}
icon="chevron-back"
aria-label="Go to previous page"
variant="clear"
/>
</li>
{pages.map((page: PaginationItem) => {
if (page.type === 'ELLIPSIS') {
return (
<li
key={page.type}
className="w-40 flex-row items-end justify-center"
>
<Icon icon="three-dots" size={16} alt="Hidden pages" />
</li>
);
}
if (page.type === 'ELLIPSIS_SMALL') {
return (
<li
key={page.type}
className="w-40 flex-row items-end justify-center lg:hidden"
>
<Icon icon="three-dots" size={16} alt="Hidden pages" />
</li>
);
}
return (
<li
key={page.value}
className={page.isAdditional ? 'until-lg:hidden' : ''}
>
<button
type="button"
aria-label={`Go to page ${page.value}`}
aria-current={page.isActive ? 'page' : undefined}
className="icon-button-clear current:bg-surface-neutral current:text-inverted"
onClick={() => setPage(page.value as number)}
>
{page.value}
</button>
</li>
);
})}
<li>
<IconButton
{...nextButtonProps}
icon="chevron-forward"
aria-label="Go to next page"
variant="clear"
/>
</li>
</ul>
</nav>
);
}

Use dots for carousels where each dot represents a single visible item. Best for 5 or fewer items.

<ul class="pagination-dots">
<li>
<button type="button" aria-current="true">
<span class="sr-only">Go to slide 1</span>
</button>
</li>
<li>
<button type="button">
<span class="sr-only">Go to slide 2</span>
</button>
</li>
<li>
<button type="button">
<span class="sr-only">Go to slide 3</span>
</button>
</li>
<li>
<button type="button">
<span class="sr-only">Go to slide 4</span>
</button>
</li>
<li>
<button type="button">
<span class="sr-only">Go to slide 5</span>
</button>
</li>
</ul>

Apply the pagination-dots class to a container of buttons. Mark the active dot with aria-current="true".

<div class="pagination-dots">
<button aria-current="true"><span class="sr-only">Slide 1</span></button>
<button><span class="sr-only">Slide 2</span></button>
<button><span class="sr-only">Slide 3</span></button>
</div>

Combine with useSnapIndicators and useSnapNavigation for a complete carousel:

Frame 1

Frame 2

Frame 3

Frame 4

Frame 5

Frame 6

Frame 7

    import {
    useSnapIndicators,
    useSnapNavigation,
    } from '@volvo-cars/react-headless';
    import { IconButton } from '@volvo-cars/react-icons';
    import { useRef } from 'react';
    export function PaginationDotsCarousel() {
    const ref = useRef<HTMLDivElement>(null);
    const { previousButtonProps, nextButtonProps } = useSnapNavigation({ ref });
    const { indicators } = useSnapIndicators({ ref });
    const count = 7;
    return (
    <section
    className="container-sm stack-16 py-s"
    data-bleed="until-md"
    aria-label="Carousel"
    >
    <div ref={ref} className="reel scrollbar-none">
    {[...Array(count).keys()].map((key, index) => (
    <div
    id={`dots-frame-${index + 1}`}
    className="snap-center aspect-4/3 w-full flex-shrink-0 flex items-center justify-center bg-feedback-green"
    key={key}
    >
    <h3 className="text-center" id={`dots-frame-${index + 1}-title`}>
    Frame {index + 1}
    </h3>
    </div>
    ))}
    </div>
    <ul className="pagination-dots">
    {indicators.map((props, index) => (
    <li key={props.id}>
    <button {...props}>
    <span className="sr-only">Go to frame {index + 1}</span>
    </button>
    </li>
    ))}
    </ul>
    <div className="container-sm flex gap-16 mt-24 justify-end">
    <IconButton
    icon="chevron-back"
    variant="filled"
    color="subtle"
    {...previousButtonProps}
    />
    <IconButton
    icon="chevron-forward"
    variant="filled"
    color="subtle"
    {...nextButtonProps}
    />
    </div>
    </section>
    );
    }

    Display position as text (e.g., “3 / 10”) for slideshows or galleries where the total count matters.

    Frame 1

    Frame 2

    Frame 3

    Frame 4

    Frame 5

    Frame 6

    Frame 7

    1/0
    import {
    useSnapIndicators,
    useSnapNavigation,
    } from '@volvo-cars/react-headless';
    import { IconButton } from '@volvo-cars/react-icons';
    import { useRef } from 'react';
    export function PaginationNumbers() {
    const ref = useRef<HTMLDivElement>(null);
    const { previousButtonProps, nextButtonProps } = useSnapNavigation({ ref });
    const { indicators, activeIndex } = useSnapIndicators({ ref });
    return (
    <div className="container-sm py-s">
    <section className="stack-16" data-bleed="until-md" aria-label="Carousel">
    <div ref={ref} className="reel scrollbar-none">
    {[...Array(7).keys()].map((key, index) => (
    <div
    key={key}
    id={`numbers-frame-${index + 1}`}
    className="snap-center aspect-4/3 w-full flex-shrink-0 flex items-center justify-center bg-feedback-green"
    >
    <h3
    className="text-center"
    id={`numbers-frame-${index + 1}-title`}
    >
    Frame {index + 1}
    </h3>
    </div>
    ))}
    </div>
    <div className="font-16 mt-24 flex justify-center">
    {activeIndex + 1}/{indicators.length}
    </div>
    <div className="container-sm flex gap-16 justify-end">
    <IconButton
    icon="chevron-back"
    variant="filled"
    color="subtle"
    {...previousButtonProps}
    />
    <IconButton
    icon="chevron-forward"
    variant="filled"
    color="subtle"
    {...nextButtonProps}
    />
    </div>
    </section>
    </div>
    );
    }

    Use activeIndex from useSnapIndicators to show the current position and indicators.length for the total.

    Combine pagination dots with an absolute-positioned pill-filled overlay showing the numeric position:

    1 / 0

    Frame 1

    Frame 2

    Frame 3

    Frame 4

    Frame 5

    Frame 6

    Frame 7

      import {
      useSnapIndicators,
      useSnapNavigation,
      } from '@volvo-cars/react-headless';
      import { IconButton } from '@volvo-cars/react-icons';
      import { useRef } from 'react';
      export function PaginationNumbersFilled() {
      const ref = useRef<HTMLDivElement>(null);
      const { previousButtonProps, nextButtonProps } = useSnapNavigation({ ref });
      const { indicators, activeIndex } = useSnapIndicators({ ref });
      return (
      <div className="container-sm relative py-s">
      <span className="pill-filled absolute z-overlay mt-24 end-24">
      {activeIndex + 1} / {indicators.length}
      </span>
      <section className="stack-16" data-bleed="until-md" aria-label="Carousel">
      <div ref={ref} className="reel scrollbar-none">
      {[...Array(7).keys()].map((key, index) => (
      <div
      id={`filled-frame-${index + 1}`}
      className="snap-center aspect-4/3 w-full flex-shrink-0 flex items-center justify-center bg-feedback-green"
      key={key}
      >
      <h3
      className="text-center"
      id={`filled-frame-${index + 1}-title`}
      >
      Frame {index + 1}
      </h3>
      </div>
      ))}
      </div>
      <ul className="pagination-dots">
      {indicators.map((props, index) => {
      return (
      <li key={props.id}>
      <button {...props}>
      <span className="sr-only">Go to frame {index + 1}</span>
      </button>
      </li>
      );
      })}
      </ul>
      <div className="container-sm flex gap-16 mt-24 justify-end">
      <IconButton
      icon="chevron-back"
      variant="filled"
      color="subtle"
      {...previousButtonProps}
      />
      <IconButton
      icon="chevron-forward"
      variant="filled"
      color="subtle"
      {...nextButtonProps}
      />
      </div>
      </section>
      </div>
      );
      }

      • Container label — add aria-label="Pagination" to the <nav> wrapper
      • Active indicator — use aria-current="page" (page numbers) or aria-current="true" (dots)
      • Button labels — provide descriptive text, e.g., aria-label="Go to page 5" or use sr-only spans for dots
      • Keyboard — ensure Tab navigates between buttons and Enter/Space activates them