Pagination
Navigate through paginated result sets. The component is a thin UI layer over Laravel's Length — pass the paginator instance and the component renders the appropriate navigation controls.
Basic Usage
In your Livewire component (or controller):
public function render()
{
return view('livewire.users', [
'users' => User::paginate(15),
]);
}
In the Blade view:
<x-wirekit::pagination :paginator="$users" />
The component renders nothing if the paginator only has a single page.
Variants
Full (default)
Shows numbered page links, prev/next arrows, and a "Showing X to Y of Z results" summary. Use for long lists where jumping to a specific page matters.
Simple
Shows prev/next buttons and a "Page X of Y" label. Ideal for infinite scrolls or when jumping to arbitrary pages is not needed.
Mini
Just prev/next buttons — the most compact form, useful in tight layouts like sidebar widgets.
Alignment
The justify prop controls how the summary text and page buttons are spread within the container:
The full variant with justify="between" (default) spreads the "Showing X to Y" summary to the left and page numbers to the right — the most common table layout pattern.
Width & Layout
The pagination stretches to fill its parent width (w-full), which allows justify="between" to spread its children apart. Wrap it in a constrained container if you need a narrower pagination area.
In a Livewire Component
Every page control is a link, so in a Livewire component a click loads the whole page again, and whatever the component holds outside the URL is gone afterwards: a filter the reader set is empty on the next page. Add livewire, and a click turns the page inside the component instead:
<x-wirekit::pagination :paginator="$orders" livewire />
Each control stays a link to its page, so it still opens in a new tab and still works before Livewire has loaded. A click calls the component's gotoPage() for the paginator's own page name, which WithPagination provides, so two paginated lists in one component turn their own pages. After the turn the page scrolls back to the top, the way Livewire's own pager does. Give scroll-to a selector to bring another element into view, or pass :scroll-to="false" to keep the reader where they are. The scroll moves as your page's scroll-behavior says, and jumps where the reader asked for reduced motion.
$paginator->links() renders the view a component names in pagination. Name WireKit's, and every links() call in the component renders this pager:
use Livewire\Component;
use Livewire\WithPagination;
class Orders extends Component
{
use WithPagination;
public function paginationView(): string
{
return 'wirekit::pagination.livewire';
}
public function paginationSimpleView(): string
{
return 'wirekit::pagination.livewire-simple';
}
}
pagination serves simple and cursor, which render previous and next only. A scroll target passed to links() reaches these views as well, false included.
Choosing the Page Size
per-page-options puts a choice of page size beside the summary. Bind it to a property of your
Livewire component with wire:model, and read that property where you paginate:
{{-- 1. The sizes on offer, and the property the choice sets --}}
<x-wirekit::pagination
:paginator="$orders"
livewire
:per-page-options="[10, 25, 50, 100]"
wire:model.live="perPage"
/>
// 2. The component paginates with the reader's choice
public int $perPage = 25;
public function render(): View
{
return view('livewire.orders', [
'orders' => Order::query()->paginate($this->perPage),
]);
}
With livewire the change also goes back to the first page, because the page the reader was on
may not exist at the new size. Without livewire, reset it yourself in updated with
$this->reset. Check the value before you paginate with it: wire:model sets whatever the
browser sends.
The size the list is using is always one of the choices, even when it is not in the list you passed, so the control never shows a size the list is not using. And the pager stays when everything fits one page, as the summary and the choice without page links: otherwise picking the largest size would remove the control that picks a smaller one again. An empty list still renders no pager.
Outside Livewire, leave wire:model off. The choice is then a small form that sends it as
per_page (rename it with per-page-name), keeps the query string your page links carry, and
drops the page number, so the list starts at its first page. The form has its own Apply button,
and picking a size does not send it: on Windows and Linux the arrow keys change a closed select's
value, so a select that loaded a new page on every change would reload the list at each key and
put the reader back at the top of the page:
A List That Fits One Page
A pager with nothing to page through renders nothing, so a list of seven results does not say it
has seven while a list of thirty does. summary-on-single-page keeps the summary there:
{{-- 1. The count stays when every result fits one page --}}
<x-wirekit::pagination :paginator="$orders" summary-on-single-page />
The summary then stands on its own, outside a navigation landmark, because there is nothing to
navigate. An empty list still renders nothing, since its own empty state speaks. A paginator that
does not know its total, such as the one simple returns or a cursor paginator, has no
summary to show. With per-page-options the summary is there on a single page anyway, beside the
choice.
Translation
Every visible string runs through Laravel's __(), so the component follows your
app locale with no props to set. WireKit ships catalogs for several languages, so
these strings may already be translated for you — see Localization.
To change the wording, add the keys to your app's own JSON language files
(lang/de.json, lang/fr.json, …); yours win per key.
Each key carries the wirekit:: prefix, which is part of the key rather than a
separate namespace — write it exactly as shown or the entry will not be found.
| Key | Where it appears |
|---|---|
wirekit::Pagination |
the nav landmark's aria-label |
wirekit::Showing :first to :last of :total results |
the summary line (full variant), one form per number, chosen by the total |
wirekit::Page :current of :last |
the centered label (simple variant) |
wirekit::Previous / wirekit::Next |
the prev/next controls and their aria-labels |
wirekit::Go to page :page |
each numbered link's aria-label |
wirekit::Per page |
the words that name the page-size choice |
wirekit::Apply |
the button that sends the page-size choice when scripts are off |
{
"wirekit::Pagination": "Seitennummerierung",
"wirekit::Showing :first to :last of :total results": "Zeige :first bis :last von :total Ergebnis|Zeige :first bis :last von :total Ergebnissen",
"wirekit::Page :current of :last": "Seite :current von :last",
"wirekit::Previous": "Zurück",
"wirekit::Next": "Weiter",
"wirekit::Go to page :page": "Gehe zu Seite :page"
}
The two sentences are single keys with placeholders rather than assembled from separate words, so you can reorder them freely — a locale that needs the total before the range, or a different particle between the numbers, writes exactly that. The numbers keep their emphasis wherever you move the placeholders.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
paginator |
LengthAwarePaginator |
null |
The Laravel paginator instance (required) |
variant |
string | 'full' |
'full', 'simple', 'mini' |
justify |
string | 'between' |
Flex layout: 'between' (summary left, pages right), 'center', 'start', 'end' |
previous-label |
string | null |
Word for the backward control. Previous is correct on a page-ordered list and misleading on a reverse-chronological one, where moving "next" goes backward in time — on a changelog or an activity feed, previous-label="Newer" says what the button does. Leave it unset to keep the translated default. |
next-label |
string | null |
Word for the forward control — the companion to previous-label. |
livewire |
boolean | false |
Turn pages inside a Livewire component. Each control keeps its link, and a click calls the component's gotoPage() for the paginator's page name. See In a Livewire Component. |
scroll-to |
string|false | 'body' |
With livewire: the selector brought into view after a page turn, looked up among the pager's ancestors first and then in the page. false keeps the scroll position. |
per-page-options |
array|string|null | null |
Sizes to choose from beside the summary, e.g. [10, 25, 50, 100]. Bind the choice with wire:model on the pager. See Choosing the Page Size. |
per-page-name |
string | 'per_page' |
The query parameter that carries the choice when the pager is not bound with wire:model. |
summary-on-single-page |
boolean | false |
Show the summary when every result fits one page. See A List That Fits One Page. |
scope |
string|null | null |
Scoped personalization name |
Paginator Object Reference
The component reads the following methods/properties from the paginator; all are provided automatically by Laravel's Model:: and DB:::
| Accessor | Used for |
|---|---|
hasPages() |
Early-return when the collection fits on a single page (component renders nothing) |
currentPage() |
Highlight the current page with aria-current="page" |
lastPage() |
Render the "Page X of Y" summary in simple variant |
firstItem() / lastItem() |
Render the "Showing X to Y" range in full variant |
total() |
Render the "of Z results" total in full variant |
previousPageUrl() / nextPageUrl() |
Prev/next arrow hrefs |
linkCollection() |
Numbered page links (trimmed to exclude prev/next) |
getPageName() / getCursorName() |
The page name a Livewire page turn is sent for (livewire only) |
A Paginator from simple and a CursorPaginator from cursor know whether another page exists, but not how many there are. The component renders mini for both, whatever variant asks for, because previous and next are all they can drive. Pass variant="mini" to say so in your markup.
Accessibility
- The outer element is
<nav role="navigation" aria-label="Pagination">— screen readers announce this as a navigation landmark - The current page uses
aria-current="page"per WAI-ARIA Authoring Practices - In the
fullvariant the prev/next controls are glyphs, so they carryaria-label="Previous"/aria-label="Next"; thesimpleandminivariants render the words themselves and are named by their own text - A prev/next control at a boundary is rendered as
<span role="link" aria-disabled="true">instead of<a>— it cannot be focused or followed, and a screen reader announces it as an unavailable link rather than skipping it, so a reader on the first or last page learns they are at the edge just as a sighted reader does from the dimmed control - Ellipses (
…) are rendered witharia-hidden="true"(decorative)
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus through page links and prev/next buttons |
Enter |
Follow the focused link |
Pagination renders as a list of native links and buttons — keyboard interaction is delegated to the browser.
Pitfalls
- Outside Livewire, keep a pager with
per-page-optionsout of your own<form>. Its page-size choice is a form of its own, and a browser does not nest forms: inside yours, the pager's form is dropped, Apply sends your form, and the controls after the pager are no longer part of your form. Place the pager after your form closes, or bind the choice withwire:model. - Don't drive pagination via
wire:clickevents on each link. The component renders native<a>elements pointing at real URLs (Laravel'slink);Collection() wire:clickwould intercept browser navigation and break the back button. - Don't show all 100 pages. The component already truncates with ellipses (
…) — overriding the truncation defeats accessibility (every link must fit in the keyboard tab order). - A cursor paginator always renders as
mini, whatever you ask for.simpleandfullstate a page number or a total, and aCursorPaginatorhas neither by design — that is the point of cursor pagination, which exists for endless append-only lists that cannot afford aCOUNT(*). The component detects the shape and renders previous/next rather than failing, and says so in the log whenAPP_DEBUGis on. Passvariant="mini"to make the intent explicit and silence the note.
Design Tokens
| Element | Token |
|---|---|
| Button background | --color-wk-bg-elevated |
| Button border | --color-wk-border |
| Button hover background | --color-wk-bg-muted |
| Button hover border | --color-wk-border-hover |
| Active page background | --color-wk-accent |
| Active page text | --color-wk-accent-fg |
| Disabled state | --color-wk-bg-subtle, --color-wk-text-subtle and --color-wk-border-subtle, without an opacity |
| Text color | --color-wk-text |
| Muted text (summary) | --color-wk-text-muted |
| Button radius | --radius-wk-md |
| Button height | --size-wk-sm |
Customization
Override defaults without publishing views via config/:
'components' => [
'pagination' => [
'variant' => 'simple',
],
],
Usage & Conventions
Prop conventions — this component uses one or more of the shared semantic prop names (
intent/variant/tone/surface). See Prop naming conventions for the canonical vocabulary, alias matrix, and decision tree.