Pagination

Page navigation in three layouts: numbered pages, a simple page status, or a table-footer row with rows-per-page.

c2-pagination moves through a list, a table or a set of search results. variant picks the layout: numbered (the default) lays out previous/next around the page numbers, simple replaces the numbers with a “Page 3 of 12” status, and compact is the table-footer row — a rows-per-page select, the range of items on show and two arrows. How many pages there are comes from total-items and page-size, or from total-pages when the item count is unknown. The element owns page and reflects it, so listening to page-change is enough to fetch the next slice.

Installation

npm install @c2n/pagination

Importing @c2n/pagination also registers c2-select and c2-list-item, the rows-per-page control of the compact variant.

Usage

Reading the page

page-change fires whenever the shown page moves, from a control or from a rows-per-page change. Its detail carries startIndex and endIndex, which slice the current page straight out of a client-side array:

const pagination = document.querySelector('c2-pagination')
pagination.totalItems = rows.length

pagination.addEventListener('page-change', (event) => {
  const { startIndex, endIndex, page } = event.detail
  render(rows.slice(startIndex, endIndex))
})

With a server, use page and pageSize instead and set total-items from the response. page-size-change fires separately when the user picks another rows-per-page value; the pagination keeps the first item of the old page on screen, so showing rows 21–30 and switching to 25 per page lands on page 1, not page 3.

Inside a table

Slotted into a c2-table’s footer, the pager becomes a controlled view: the table owns page, page-size and total-items and feeds them down, so there is nothing to wire up.

<c2-table row-key="id" style="height: 420px">
  <c2-table-column field="name" header="Name"></c2-table-column>
  <c2-pagination slot="footer" variant="compact"></c2-pagination>
</c2-table>

A table holding rows slices them in place; one with a dataSource asks for a page per request — the markup is the same either way. See Table. Everything else — variant, the labels, page-size-options, the styling — is still the pager’s own, and a pager anywhere else keeps managing its own state exactly as before.

How many numbers are shown

boundary-count (default 1) is how many numbers stay pinned at each end, and sibling-count (default 1) how many surround the current page. Everything left over collapses into an ellipsis, and an ellipsis that would stand for a single page is replaced by that page — so the row keeps the same number of slots as the current page moves and nothing shifts under the pointer.

The row of controls never wraps. When the numbers those counts ask for do not fit the host’s width, the pagination sheds them — siblings first, then boundaries — until they do, and puts them back when the container grows again; the current page always survives. The counts are a ceiling, in other words, not a promise. With the defaults and 20 pages, that means 1 … 9 10 11 … 20 down to about 360px, 1 … 10 … 20 down to about 280px, and … 10 … below that. Only the compact variant may drop onto a second line, and then only between its three groups.

The ellipsis is what keeps the shed pages reachable: it is a button, and it jumps into the middle of the range it stands for — the between 1 and 9 goes to page 5. It reads as an ellipsis at rest and shows the direction of the jump on hover and keyboard focus, so the affordance is visible before the click. jump-label-template names it for screen readers ({page}, {from}, {to}, {count}).

Because boundary-count="1" keeps page 1 and the last page one click away, show-first-last has nothing to add to the numbered variant. It is there for simple and compact, which show no numbers at all — or for boundary-count="0", which gives up the pinned ends to save room.

Labels and translation

Every piece of text is an attribute: previous-label, next-label, first-label, last-label and page-size-label, plus four templates — range-template ({start}, {end}, {total}), page-template ({page}, {pageCount}), page-label-template ({page}, the accessible name of a page number) and jump-label-template (the accessible name of an ellipsis). The controls sit in a nav landmark named “Pagination” unless aria-label says otherwise, the current page carries aria-current="page", and the status text is a polite live region so a page change is announced. hide-nav-labels drops the visible “Previous”/“Next” text and keeps the accessible name, which is what the compact variant does.

Theming

--c2-pagination__item--* styles the page numbers (and __selected the current one), --c2-pagination__nav--* the previous/next/first/last controls, so the two can look different without fighting each other. --c2-pagination__label--* covers the status text and the rows-per-page label, --c2-pagination__icon--size the chevrons, and --c2-pagination--justify-content decides where the row sits in the host’s width — flex-end for a table footer. The rows-per-page select wears those same variables, so there is no need to reach for --c2-select__* to style it.

escAccordionConnected, animated panels with shared borders and single or multiple expansion.LayoutAvatarImage, initials or icon for a person, with status dot and badge.Data displayBadgeTinted pill for status text, counts and dots, optionally pinned to a corner of another element.Data displayBreadcrumbNavigation trail of link buttons with separators, a current page and optional collapsing.NavigationButtonThemeable button with slots for text, prefix, suffix and running icons.ButtonsButton GroupAttached buttons with shared borders, optionally a segmented control with single or multiple selection.ButtonsCardGroups related content and actions on a single bordered surface.LayoutChat InputAuto-growing message box with a send button for chat interfaces.ChatChat MessageOne chat bubble with avatar, title, timestamp and message body.ChatCheckboxNative checkbox behaviour in a quiet, themeable box with an opt-in hover layer.InputsCode ViewerSyntax-highlighted code with line numbers, copy button and dark mode, powered by shiki.Data displayColor AreaTwo-dimensional area for picking saturation and value of a colour.InputsColor SelectColour swatch that opens a full picker built from area and slider.InputsColor SliderHorizontal slider for choosing a hue from 0 to 360.InputsCopy ButtonButton that copies text to the clipboard — the element it sits in, another element by id, or a literal string.ButtonsDetailsCollapsible disclosure built on native details and summary.LayoutIcon ButtonRound, hoverable button wrapping a slotted SVG icon.ButtonsKbdKeyboard key label for shortcuts and command hints, with the semantics of the native kbd element.Data displayLabelCaption that names and activates the control referenced by its for attribute, with a required marker.InputsLink ButtonText-styled control for link and navigation actions, rendered as a real anchor or a button.NavigationListVertical list container with single or multiple selection.Data displayList ItemSelectable row with icon slots, used on its own or as the option of list and select.Data displayMenuCommands, links, checkboxes and submenus in a popover anchored to a trigger.NavigationModalDialog built on the native dialog element: focus trap, backdrop, Escape, title, body and footer.FeedbackNavigation MenuSite navigation bar whose triggers open panels of links below the header.NavigationOverlayAnchored popup built on the browser Popover API, positioned with floating-ui.FeedbackPaginationPage navigation in three layouts: numbered pages, a simple page status, or a table-footer row with rows-per-page.NavigationProgressLinear progress bar, indeterminate or filling to a value, with an optional label and count.FeedbackRadioRadio options built on native inputs, grouped into one value with keyboard navigation.InputsSelectDropdown that pairs a themeable trigger with an anchored list of c2-list-item options.InputsSeperatorHorizontal or vertical rule with an optional label, for dividing content and toolbars.LayoutSheetDialog pinned to an edge of the screen, for content that complements the page rather than interrupting it.FeedbackSide NavResponsive navigation drawer beside the page: pushes the content on large screens, slides over it with a backdrop on small ones.NavigationSkeletonPlaceholder block standing in for content that has not arrived, in three shapes and three animations.FeedbackSliderRange input with a themeable track, thumb, step ticks and value bubble.InputsSpinnerCircular progress indicator, indeterminate or showing a value, with optional text.FeedbackSwitchOn/off toggle on a native switch input, with label, description and thumb icons.InputsTableVirtualized data grid with declarative columns, sorting, selection, pinning and resizing.Data displayTabsTab strip that shows one content panel at a time.NavigationText FieldSingle-line input with icon slots, clear button, helper and error text, and a character counter.InputsTextareaMultiline text input with resizing, helper and error text, and a character counter.InputsToastNotification cards and a manager for stacked, queued notifications with independent lifetimes.FeedbackTooltipContextual hint shown when its target is hovered or focused, rendered in the top layer.FeedbackFeather Icons287 open-source Feather icons, one web component each.IconsMat Icon2,234 Material Icons ligatures rendered through a single element.IconsPhosphor Icons1,512 flexible icons in six weights, one web component each.IconsThemingTheme every c2n component from a handful of design tokens with @c2n/theme, or reach for any component variable directly.GuideUsing c2n in an applicationThe workflow behind every c2n app: load the theme once, use c2-* tags directly, and turn every repeated pattern into a small variant or composed component.Guide