Skip to content

Pagination

From a pagePage 5 of 27, in a request for /people?sort=name,asc&page=4. Each link keeps the sort.
Thymeleaf source
<sl:pagination th:page="${people}"/>
Rendered HTML
<nav class="pagination" aria-label="Pagination">
<ul class="pagination-content">
<li class="pagination-item">
<a class="button pagination-link pagination-previous" data-variant="ghost" aria-label="Go to previous page" href="/people?sort=name,asc&amp;page=3" id="page-previous">
<span class="button-icon" data-icon="inline-start">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m15 18-6-6 6-6"></path>
</svg>
</span>
<span class="pagination-previous-text">Previous</span>
</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/people?sort=name,asc&amp;page=0" id="page-1">1</a>
</li>
<li class="pagination-item">
<span class="pagination-ellipsis">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
<span class="sl-sr-only">More pages</span>
</span>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/people?sort=name,asc&amp;page=3" id="page-4">4</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="outline" data-size="icon" aria-current="page" href="/people?sort=name,asc&amp;page=4" id="page-5">5</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/people?sort=name,asc&amp;page=5" id="page-6">6</a>
</li>
<li class="pagination-item">
<span class="pagination-ellipsis">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
<span class="sl-sr-only">More pages</span>
</span>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/people?sort=name,asc&amp;page=26" id="page-27">27</a>
</li>
<li class="pagination-item">
<a class="button pagination-link pagination-next" data-variant="ghost" aria-label="Go to next page" href="/people?sort=name,asc&amp;page=5" id="page-next">
<span class="pagination-next-text">Next</span>
<span class="button-icon" data-icon="inline-end">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6"></path>
</svg>
</span>
</a>
</li>
</ul>
</nav>

Give <sl:pagination> the Spring Data Page the controller put in the model, and nothing else:

<sl:pagination th:page="${people}"/>

It renders Previous, the first and the last page, the current page with one page on each side, an ellipsis for the pages it leaves out, and Next. A gap of a single page shows that page instead of an ellipsis, which would take the same room. siblings sets how many pages show on each side of the current one:

More pages around the current one
Thymeleaf source
<sl:pagination th:page="${orders}" siblings="2" qualifier="orders" aria-label="Order pages"/>
Rendered HTML
<nav class="pagination" aria-label="Order pages">
<ul class="pagination-content">
<li class="pagination-item">
<a class="button pagination-link pagination-previous" data-variant="ghost" aria-label="Go to previous page" href="/orders?orders_page=8" id="orders-page-previous">
<span class="button-icon" data-icon="inline-start">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m15 18-6-6 6-6"></path>
</svg>
</span>
<span class="pagination-previous-text">Previous</span>
</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/orders?orders_page=0" id="orders-page-1">1</a>
</li>
<li class="pagination-item">
<span class="pagination-ellipsis">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
<span class="sl-sr-only">More pages</span>
</span>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/orders?orders_page=7" id="orders-page-8">8</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/orders?orders_page=8" id="orders-page-9">9</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="outline" data-size="icon" aria-current="page" href="/orders?orders_page=9" id="orders-page-10">10</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/orders?orders_page=10" id="orders-page-11">11</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/orders?orders_page=11" id="orders-page-12">12</a>
</li>
<li class="pagination-item">
<span class="pagination-ellipsis">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
<span class="sl-sr-only">More pages</span>
</span>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/orders?orders_page=26" id="orders-page-27">27</a>
</li>
<li class="pagination-item">
<a class="button pagination-link pagination-next" data-variant="ghost" aria-label="Go to next page" href="/orders?orders_page=10" id="orders-page-next">
<span class="pagination-next-text">Next</span>
<span class="button-icon" data-icon="inline-end">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6"></path>
</svg>
</span>
</a>
</li>
</ul>
</nav>

Each link is the request that rendered the page with another page number: every other parameter in the address (the sort, the page size, a search) is kept as it was. The controller needs nothing new, as the Pageable that sorts the table also reads the page:

@GetMapping("/people")
String people(@PageableDefault(size = 20, sort = "name") Pageable pageable, Model model) {
model.addAttribute("people", personRepository.findAll(pageable));
return "people";
}

Sort the rows by a unique key last, or a row can show up on two pages or on none: see a stable order across pages.

The page shown now is an outline button with aria-current="page". On the first page Previous is disabled, and on the last page Next, the way a disabled link button is: no href, aria-disabled="true", out of the tab order. An empty result still has one page, the one saying so.

On the first pagePrevious is disabled. A gap of one page shows that page rather than an ellipsis.
Thymeleaf source
<sl:pagination th:page="${team}" qualifier="team" aria-label="Team pages"/>
Rendered HTML
<nav class="pagination" aria-label="Team pages">
<ul class="pagination-content">
<li class="pagination-item">
<a class="button pagination-link pagination-previous" data-variant="ghost" aria-label="Go to previous page" aria-disabled="true" role="link" tabindex="-1" id="team-page-previous">
<span class="button-icon" data-icon="inline-start">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m15 18-6-6 6-6"></path>
</svg>
</span>
<span class="pagination-previous-text">Previous</span>
</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="outline" data-size="icon" aria-current="page" href="/team?team_page=0" id="team-page-1">1</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/team?team_page=1" id="team-page-2">2</a>
</li>
<li class="pagination-item">
<span class="pagination-ellipsis">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
<span class="sl-sr-only">More pages</span>
</span>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/team?team_page=4" id="team-page-5">5</a>
</li>
<li class="pagination-item">
<a class="button pagination-link pagination-next" data-variant="ghost" aria-label="Go to next page" href="/team?team_page=1" id="team-page-next">
<span class="pagination-next-text">Next</span>
<span class="button-icon" data-icon="inline-end">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6"></path>
</svg>
</span>
</a>
</li>
</ul>
</nav>

A Slice knows whether there is a next page, not how many pages there are, so it gets Previous and Next only. It saves the count query on a large table:

A SliceA Slice does not know how many pages there are, so it gets Previous and Next only.
Thymeleaf source
<sl:pagination th:page="${activity}" qualifier="activity" aria-label="Activity pages"/>
Rendered HTML
<nav class="pagination" aria-label="Activity pages">
<ul class="pagination-content">
<li class="pagination-item">
<a class="button pagination-link pagination-previous" data-variant="ghost" aria-label="Go to previous page" href="/activity?activity_page=0" id="activity-page-previous">
<span class="button-icon" data-icon="inline-start">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m15 18-6-6 6-6"></path>
</svg>
</span>
<span class="pagination-previous-text">Previous</span>
</a>
</li>
<li class="pagination-item">
<a class="button pagination-link pagination-next" data-variant="ghost" aria-label="Go to next page" href="/activity?activity_page=2" id="activity-page-next">
<span class="pagination-next-text">Next</span>
<span class="button-icon" data-icon="inline-end">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6"></path>
</svg>
</span>
</a>
</li>
</ul>
</nav>

A list read a Slice at a time can also grow instead: Load more appends the next slice’s rows with htmx, and is a link to the next page without it.

Without a Page, pass the page shown now (counting from 1) and the number of pages:

Without Spring Data
Thymeleaf source
<sl:pagination th:current="${results.page}" th:total="${results.pages}" qualifier="results"
aria-label="Result pages"/>
Rendered HTML
<nav class="pagination" aria-label="Result pages">
<ul class="pagination-content">
<li class="pagination-item">
<a class="button pagination-link pagination-previous" data-variant="ghost" aria-label="Go to previous page" href="/search?q=thymeleaf&amp;results_page=1" id="results-page-previous">
<span class="button-icon" data-icon="inline-start">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m15 18-6-6 6-6"></path>
</svg>
</span>
<span class="pagination-previous-text">Previous</span>
</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/search?q=thymeleaf&amp;results_page=0" id="results-page-1">1</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/search?q=thymeleaf&amp;results_page=1" id="results-page-2">2</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="outline" data-size="icon" aria-current="page" href="/search?q=thymeleaf&amp;results_page=2" id="results-page-3">3</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/search?q=thymeleaf&amp;results_page=3" id="results-page-4">4</a>
</li>
<li class="pagination-item">
<span class="pagination-ellipsis">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
<span class="sl-sr-only">More pages</span>
</span>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="/search?q=thymeleaf&amp;results_page=7" id="results-page-8">8</a>
</li>
<li class="pagination-item">
<a class="button pagination-link pagination-next" data-variant="ghost" aria-label="Go to next page" href="/search?q=thymeleaf&amp;results_page=3" id="results-page-next">
<span class="pagination-next-text">Next</span>
<span class="button-icon" data-icon="inline-end">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6"></path>
</svg>
</span>
</a>
</li>
</ul>
</nav>

The links ask for the page with the same parameter Spring Data would read, zero-based unless spring.data.web.pageable.one-indexed-parameters is set (see parameter names); the controller reads it itself. sl:pagination-summary and sl:pagination-size need a Page or Slice: without Spring Data, write the summary with #messages and the sizes as links of your own.

The qualifier of the controller’s second Pageable (@Qualifier("members")) gives a pagination its own parameter (members_page) and its links their own ids (members-page-2), as it does for the table’s headers.

With content of its own, <sl:pagination> is the nav around a pagination of parts, as in shadcn/ui:

Written by handThe parts, as in shadcn/ui's example.
Thymeleaf source
<sl:pagination aria-label="Invoice pages">
<sl:pagination-content>
<sl:pagination-item>
<sl:pagination-previous href="#"/>
</sl:pagination-item>
<sl:pagination-item>
<sl:pagination-link href="#">1</sl:pagination-link>
</sl:pagination-item>
<sl:pagination-item>
<sl:pagination-link href="#" active>2</sl:pagination-link>
</sl:pagination-item>
<sl:pagination-item>
<sl:pagination-link href="#">3</sl:pagination-link>
</sl:pagination-item>
<sl:pagination-item>
<sl:pagination-ellipsis/>
</sl:pagination-item>
<sl:pagination-item>
<sl:pagination-next href="#"/>
</sl:pagination-item>
</sl:pagination-content>
</sl:pagination>
Rendered HTML
<nav class="pagination" aria-label="Invoice pages">
<ul class="pagination-content">
<li class="pagination-item">
<a class="button pagination-link pagination-previous" data-variant="ghost" aria-label="Go to previous page" href="#">
<span class="button-icon" data-icon="inline-start">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m15 18-6-6 6-6"></path>
</svg>
</span>
<span class="pagination-previous-text">Previous</span>
</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="#">1</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="outline" data-size="icon" aria-current="page" href="#">2</a>
</li>
<li class="pagination-item">
<a class="button pagination-link" data-variant="ghost" data-size="icon" href="#">3</a>
</li>
<li class="pagination-item">
<span class="pagination-ellipsis">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
<span class="sl-sr-only">More pages</span>
</span>
</li>
<li class="pagination-item">
<a class="button pagination-link pagination-next" data-variant="ghost" aria-label="Go to next page" href="#">
<span class="pagination-next-text">Next</span>
<span class="button-icon" data-icon="inline-end">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6"></path>
</svg>
</span>
</a>
</li>
</ul>
</nav>

sl:pagination-link takes active for the page shown now and disabled for nowhere to go; sl:pagination-previous and sl:pagination-next take disabled, and content of their own replaces their text.

<sl:pagination-summary> says which rows the page shows, or with format="page" which page it is:

Summary
Thymeleaf source
<sl:pagination-summary th:page="${people}"/>
Rendered HTML
<div class="pagination-summary">11–20 of 270</div>
Summary as the page number
Thymeleaf source
<sl:pagination-summary th:page="${people}" format="page"/>
Rendered HTML
<div class="pagination-summary">Page 2 of 27</div>

A Slice has no total, so it reads “11–20” or “Page 2”. The words are messages, so your messages.properties rewords or translates them; the numbers are formatted for the locale (“1,001–1,010 of 12,345”).

<sl:pagination-size> is a label and a dropdown menu of sizes, as in shadcn/ui’s data table:

Rows per pageIn a request for /people?sort=name,asc&page=3&size=20. Each size keeps the sort and starts on the first page.
Thymeleaf source
<sl:pagination-size th:page="${people}"/>
Rendered HTML
<div class="pagination-size">
<span class="pagination-size-label" id="page-size-label">Rows per page</span>
<button class="button dropdown-menu-trigger pagination-size-trigger" type="button" data-variant="outline" data-size="sm" id="page-size-trigger" popovertarget="page-size" aria-haspopup="menu" aria-labelledby="page-size-label page-size-trigger">
20
<span class="button-icon" data-icon="inline-end">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</span>
</button>
<div class="dropdown-menu-content pagination-size-content" id="page-size" role="menu" popover tabindex="-1" x-data="slDropdownMenu" aria-labelledby="page-size-label" data-side="top">
<div class="dropdown-menu-radio-group" role="group">
<a class="dropdown-menu-radio-item" role="menuitemradio" aria-checked="false" href="/people?sort=name,asc&amp;size=10">
10
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
<a class="dropdown-menu-radio-item" role="menuitemradio" aria-checked="true" href="/people?sort=name,asc&amp;size=20">
20
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
<a class="dropdown-menu-radio-item" role="menuitemradio" aria-checked="false" href="/people?sort=name,asc&amp;size=50">
50
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
</div>
</div>
</div>

Each size is a link to the same request with that size, every other parameter kept except the page number, so the list starts on its first page. It works without JavaScript: the menu is a popover. sizes="10,20,50" is the default; a size the page has that is not in the list is added to it, so the menu always shows a checked one. Limit what the controller accepts with spring.data.web.pageable.max-page-size.

Summary, size and pagination are separate, so you lay out the footer. shadcn/ui’s data table puts the summary at the start and the controls at the end:

<div class="table-footer">
<sl:pagination-summary th:page="${people}"/>
<div class="table-footer-controls">
<sl:pagination-size th:page="${people}"/>
<sl:pagination th:page="${people}"/>
</div>
</div>
.table-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; }
.table-footer-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; }
/* The pagination centres itself across the full width by default. */
.table-footer .pagination { inline-size: auto; margin: 0; }

Sample-01’s /people page does this.

Every link has an id: page-previous, page-next and page-<number> (prefixed with the qualifier). When htmx replaces the list and its pagination, it gives the focus back to the element with the id that had it, so after Next the focus is on the new Next. The size menu’s button is page-size-trigger: the menu gives the focus back to it before the chosen link is followed. Tables with htmx shows the whole page.

  • The nav is named “Pagination” (sl.pagination.label); an aria-label of your own replaces it, which two paginations on one page need.
  • Previous and Next are named “Go to previous page” and “Go to next page”. On a narrow screen only their chevron shows.
  • The ellipsis reads “More pages”, where shadcn/ui hides it from screen readers: a list item that says nothing is harder to follow.
  • The size menu’s button reads “Rows per page” followed by the size.
Key English
sl.pagination.label Pagination
sl.pagination.previous, sl.pagination.previous.label Previous, Go to previous page
sl.pagination.next, sl.pagination.next.label Next, Go to next page
sl.pagination.more More pages
sl.pagination.summary {0}–{1} of {2}
sl.pagination.summary.slice {0}–{1}
sl.pagination.summary.empty 0 of 0
sl.pagination.summary.page, sl.pagination.summary.page.slice Page {0} of {1}, Page {0}
sl.pagination-size.label Rows per page

First and last page buttons (shadcn’s dashboard has them; the first and the last page are always among the numbers), a field to type a page number, and paging in the browser.

AttributeValuesDefaultDescription
th:pageobject—The Spring Data Page or Slice shown now.
currentnumber—Without a page: the page shown now, counting from 1.
totalnumber—Without a page: the number of pages.
siblingsnumber1How many pages to show on each side of the current one.
qualifierstring—The @Qualifier of the controller's Pageable, for a second list on the page: its links use members_page instead of page, and their ids start with members-.

Every prop can also be set from an expression: th:page="${...}". Any other attribute passes through to the rendered element, and class is appended.

sl:pagination-content declares no props; every attribute passes through.

sl:pagination-item declares no props; every attribute passes through.

AttributeValuesDefaultDescription
activeboolean—Whether it is the page shown now: aria-current="page" and the outline look.
sizexs sm default lg icon icon-xs icon-sm icon-lgiconSize of the link, as on sl:button: square by default, for a number.
disabledboolean—There is nowhere to go: the link loses its href, gets aria-disabled and leaves the tab order, as a disabled sl:button as="a".

Every prop can also be set from an expression: th:active="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
disabledboolean—There is no previous page: the link loses its href, gets aria-disabled and leaves the tab order.

Every prop can also be set from an expression: th:disabled="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
disabledboolean—There is no next page: the link loses its href, gets aria-disabled and leaves the tab order.

Every prop can also be set from an expression: th:disabled="${...}". Any other attribute passes through to the rendered element, and class is appended.

sl:pagination-ellipsis declares no props; every attribute passes through.

AttributeValuesDefaultDescription
th:pageobject—The Spring Data Page or Slice shown now.
formatrange pagerangerange: the rows shown and the total. page: the page number and the number of pages.

Every prop can also be set from an expression: th:page="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
th:pageobject—The Spring Data Page or Slice shown now.
sizesstring10,20,50The sizes to choose from, separated by commas.
qualifierstring—The @Qualifier of the controller's Pageable, for a second list on the page: its links use members_size instead of size.
idstring—The menu's id; its button gets -trigger added, its label -label. page-size when not set, prefixed with the qualifier (members-page-size).

Every prop can also be set from an expression: th:page="${...}". Any other attribute passes through to the rendered element, and class is appended.