Skip to content
Brok UI

Loading…

No results

List Footer

Open source

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.

Version
v1.0.1
Stability
stable
License
MIT
Related
Admin Table
Pagination
List Toolbar

Preview

Showing 1–25 of 63

previews.components.admin-list-footer.default.blade.php Blade
<div class="w-full">
    <x-ui.admin.list-footer />
</div>

Installation

terminal
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:

resources/js/ui/index.js JS
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.

  • blade resources/views/components/ui/admin/list-footer.blade.php
Registry dependencies
button select kbd
Packages
composer: jml/brok:^0.2

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.

admin-list-footer.md
# 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

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
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.

list-footer list-footer-keys list-footer-readout

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

Layout and content structure

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
list-footer list-footer-readout list-footer-keys
Theming hooks
list-footer

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
managed
Focus
none
  • Disabled stepping keeps the buttons rendered and announced as disabled instead of removing them.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, 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 under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<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.

resources/views/components/ui/admin/list-footer.blade.php Blade
{{--
    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>&ndash;<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>&ndash;<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