List Footer
The list readout — showing from–to of matched (filtered from total), a keyboard legend, a per-page select and previous/next — server-rendered or bound live to the list's Alpine state.
Preview
Showing 1–25 of 63
j next k previous Enter open Esc close
<div class="w-full">
<x-ui.admin.list-footer />
</div>
Installation
php artisan ui:add admin-list-footer
Note
This component ships an Alpine behavior module at
resources/js/ui/admin-list-footer.js. Import it once from your bundle so it registers on alpine:init:
import './admin-list-footer.js';
Registry contract
php artisan ui:add admin-list-footer
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/admin/list-footer.blade.php
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: List Footer (`admin-list-footer`)
The list readout — showing from–to of matched (filtered from total), a keyboard legend, a per-page select and previous/next — server-rendered or bound live to the list's Alpine state.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add admin-list-footer
```
## Usage
```blade
<div class="w-full">
<x-ui.admin.list-footer />
</div>
```
## Props
- `from` (int, default `1`) — First visible row number.
- `to` (int, default `25`) — Last visible row number.
- `matched` (int, default `63`) — Rows matching the current view.
- `total` (int|null, default `null`) — Rows before filtering; when it differs from `matched` the readout says so.
- `page` (int, default `1`) — Current page.
- `pages` (int, default `3`) — Page count.
- `perPage` (int, default `25`) — Rows per page.
- `perPageOptions` (int[], default `[10, 25, 50, 100]`) — Per-page choices.
- `perPageAction` (string|null, default `null`) — GET target of the per-page form.
- `perPageName` (string, default `per_page`) — Per-page input name.
- `previousHref` (string|null, default `null`) — Previous page link; defaults to `?page=N-1`. Disabled on page 1.
- `nextHref` (string|null, default `null`) — Next page link; defaults to `?page=N+1`. Disabled on the last page.
- `keys` (array|null, default `null`) — [['key','label']] keyboard legend; `[]` hides it.
- `live` (bool, default `false`) — Bind the readout to `from`, `to`, `matched`, `total`, `page`, `pages` and call `prev()`, `next()`, `setPerPage()` in the surrounding Alpine scope.
## Use when
- Use to structure hierarchy, spacing, and responsiveness so content is easier to scan and navigate.
- The `footer` slot of an admin table on any list screen.
- A list that filters in place and needs its counts bound to live state — pass `live`.
## Avoid when
- Do not let layout primitives substitute for semantics, headings, or interaction rules users still need.
- Numbered page links for a long public listing — use `pagination`.
## Anti-patterns
- Using visual layout as a substitute for semantic structure
## Rules
- Use the `<brok:admin-list-footer>` tag (or `<x-ui.admin-list-footer>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/admin-list-footer
- Registry JSON (files, props, contract): https://brokui.dev/r/open/admin-list-footer.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| from | int | 1 | First visible row number. |
| to | int | 25 | Last visible row number. |
| matched | int | 63 | Rows matching the current view. |
| total | int | null | null | Rows before filtering; when it differs from `matched` the readout says so. |
| page | int | 1 | Current page. |
| pages | int | 3 | Page count. |
| perPage | int | 25 | Rows per page. |
| perPageOptions | int[] | [10, 25, 50, 100] | Per-page choices. |
| perPageAction | string | null | null | GET target of the per-page form. |
| perPageName | string | per_page | Per-page input name. |
| previousHref | string | null | null | Previous page link; defaults to `?page=N-1`. Disabled on page 1. |
| nextHref | string | null | null | Next page link; defaults to `?page=N+1`. Disabled on the last page. |
| keys | array | null | null | [['key','label']] keyboard legend; `[]` hides it. |
| live | bool | false | Bind the readout to `from`, `to`, `matched`, `total`, `page`, `pages` and call `prev()`, `next()`, `setPerPage()` in the surrounding Alpine scope. |
Slots
Default Blade slot only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- The readout region is `aria-live="polite"`, so a screen reader hears the new range after a filter or page change.
- In server mode the per-page select submits its own GET form on change.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized.
Guidance
Structure content hierarchy and responsive relationships.
Use when
- Use to structure hierarchy, spacing, and responsiveness so content is easier to scan and navigate.
- The `footer` slot of an admin table on any list screen.
- A list that filters in place and needs its counts bound to live state — pass `live`.
Avoid when
- Do not let layout primitives substitute for semantics, headings, or interaction rules users still need.
- Numbered page links for a long public listing — use `pagination`.
Use instead
- Semantic HTML with standard flow
Anti-patterns
- Using visual layout as a substitute for semantic structure
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- managed
- Focus
none
- Disabled stepping keeps the buttons rendered and announced as disabled instead of removing them.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="admin-list-footer-{{ $record->id }}">
<div class="w-full">
<x-ui.admin.list-footer />
</div>
</div>
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
{{--
List Footer — what you are looking at, how to move through it, and the
keyboard route for people who live in this screen.
The readout says "showing 1–25 of 63" and, when a view or filter has
narrowed the list, "(filtered from 412)" — the two numbers an operator
otherwise has to reconstruct from the toolbar. The keyboard legend is
the honest version of "power users can use j/k": it is written where the
hand already is, on every list, in the same words.
Two modes, one markup:
- server (default): counts and page links are props, per-page is a GET
form that submits on change, previous/next are real links.
- `live`: the readout binds to `from`, `to`, `matched`, `total`,
`page`, `pages` in the surrounding Alpine scope and the controls call
`prev()`, `next()` and `setPerPage()` there, for a list that filters
in place without a round trip.
Generalised from the Noord-C admin's `list-footer.blade.php`, copied
into twenty-one modules; belongs in `<x-slot:footer>` of an admin table.
--}}
@props([
'from' => 1,
'to' => 25,
'matched' => 63,
'total' => null,
'page' => 1,
'pages' => 3,
'perPage' => 25,
'perPageOptions' => [10, 25, 50, 100],
'perPageAction' => null,
'perPageName' => 'per_page',
'previousHref' => null,
'nextHref' => null,
'keys' => null,
'live' => false,
])
@php
// Shapes documented in item.json knowledge.props:
// keys: [['key' => 'j', 'label' => 'next row'], …] — the keyboard legend; null renders the j/k/Enter/Esc default, [] hides it.
$keys ??= [
['key' => 'j', 'label' => __('next')],
['key' => 'k', 'label' => __('previous')],
['key' => 'Enter', 'label' => __('open')],
['key' => 'Esc', 'label' => __('close')],
];
$total ??= $matched;
$filtered = (int) $total !== (int) $matched;
// Plain `?page=` links by default: the shape Laravel's paginator emits, so
// a list works before the host wires anything. The bounds decide whether
// a step is possible; the href only says where it goes.
$previousHref ??= '?page='.max(1, (int) $page - 1);
$nextHref ??= '?page='.min((int) $pages, (int) $page + 1);
$previousDisabled = $live ? null : (int) $page <= 1;
$nextDisabled = $live ? null : (int) $page >= (int) $pages;
@endphp
<div
data-slot="list-footer"
data-surface="admin"
@if ($live) data-live="true" @endif
{{ $attributes->merge(['class' => 'flex min-w-0 flex-wrap items-center gap-x-4 gap-y-2 text-xs text-muted-foreground']) }}
>
<p data-slot="list-footer-readout" class="min-w-0" aria-live="polite">
@if ($live)
{{ __('Showing') }}
<span class="font-medium text-foreground tabular-nums" x-text="from"></span>–<span class="font-medium text-foreground tabular-nums" x-text="to"></span>
{{ __('of') }}
<span class="font-medium text-foreground tabular-nums" x-text="matched"></span>
<span x-show="matched !== total" x-cloak>{{ __('(filtered from') }} <span class="tabular-nums" x-text="total"></span>)</span>
@else
{{ __('Showing') }}
<span class="font-medium text-foreground tabular-nums">{{ $from }}</span>–<span class="font-medium text-foreground tabular-nums">{{ $to }}</span>
{{ __('of') }}
<span class="font-medium text-foreground tabular-nums">{{ $matched }}</span>
@if ($filtered)
<span>{{ __('(filtered from :total)', ['total' => $total]) }}</span>
@endif
@endif
</p>
@if ($keys !== [])
<p data-slot="list-footer-keys" class="hidden items-center gap-2 lg:flex">
@foreach ($keys as $hint)
<span class="inline-flex items-center gap-2">
<x-ui.kbd>{{ $hint['key'] ?? '' }}</x-ui.kbd>
<span>{{ $hint['label'] ?? '' }}</span>
</span>
@endforeach
</p>
@endif
<div class="ms-auto flex min-w-0 flex-wrap items-center gap-2">
@if ($live)
<label class="flex items-center gap-2">
<span class="sr-only">{{ __('Rows per page') }}</span>
<x-ui.select size="sm" :name="$perPageName" x-on:change="setPerPage($event.target.value)">
@foreach ($perPageOptions as $option)
<option value="{{ $option }}" @selected((int) $option === (int) $perPage)>{{ __(':count / page', ['count' => $option]) }}</option>
@endforeach
</x-ui.select>
</label>
<nav class="flex items-center gap-2" aria-label="{{ __('Page navigation') }}">
<x-ui.button variant="outline" size="sm" x-on:click="prev()" x-bind:disabled="page <= 1">{{ __('Previous') }}</x-ui.button>
<span class="px-2 tabular-nums">
<span class="font-medium text-foreground" x-text="page"></span>
{{ __('of') }}
<span x-text="pages"></span>
</span>
<x-ui.button variant="outline" size="sm" x-on:click="next()" x-bind:disabled="page >= pages">{{ __('Next') }}</x-ui.button>
</nav>
@else
<form method="GET" action="{{ $perPageAction ?? '' }}" class="flex items-center gap-2">
<label class="flex items-center gap-2">
<span class="sr-only">{{ __('Rows per page') }}</span>
<x-ui.select size="sm" :name="$perPageName" onchange="this.form.requestSubmit()">
@foreach ($perPageOptions as $option)
<option value="{{ $option }}" @selected((int) $option === (int) $perPage)>{{ __(':count / page', ['count' => $option]) }}</option>
@endforeach
</x-ui.select>
</label>
<noscript><x-ui.button type="submit" variant="outline" size="sm">{{ __('Apply') }}</x-ui.button></noscript>
</form>
<nav class="flex items-center gap-2" aria-label="{{ __('Page navigation') }}">
<x-ui.button variant="outline" size="sm" :href="$previousDisabled ? null : $previousHref" :disabled="$previousDisabled" rel="prev">{{ __('Previous') }}</x-ui.button>
<span class="px-2 tabular-nums" aria-current="page">
<span class="font-medium text-foreground">{{ $page }}</span>
{{ __('of') }}
{{ $pages }}
</span>
<x-ui.button variant="outline" size="sm" :href="$nextDisabled ? null : $nextHref" :disabled="$nextDisabled" rel="next">{{ __('Next') }}</x-ui.button>
</nav>
@endif
</div>
</div>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.1 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0