Page Bar
The bar that spans only the main column — breadcrumb trail or title said once, record-to-record stepping, an ambient integration-health pill, role, locale, theme and notifications.
Preview
<div class="w-full">
<x-ui.admin.page-bar />
</div>
Installation
php artisan ui:add admin-page-bar
Note
This component ships an Alpine behavior module at
resources/js/ui/admin-page-bar.js. Import it once from your bundle so it registers on alpine:init:
import './admin-page-bar.js';
Registry contract
php artisan ui:add admin-page-bar
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/page-bar.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: Page Bar (`admin-page-bar`)
The bar that spans only the main column — breadcrumb trail or title said once, record-to-record stepping, an ambient integration-health pill, role, locale, theme and notifications.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add admin-page-bar
```
## Usage
```blade
<div class="w-full">
<x-ui.admin.page-bar />
</div>
```
## Props
- `title` (string, default `Orders`) — Plain title, shown when `trail` is empty.
- `trail` (array, default `null`) — [['label','href' => null]] — ancestors carry an href, the current page does not. `null` renders a sample trail; `[]` renders the plain title.
- `record` (array|null, default `null`) — ['previous' => ['label','href'], 'next' => ['label','href']] — record-to-record stepping. `null` renders a sample pair; `[]` renders none.
- `integrations` (array|null, default `null`) — ['healthy','total','href'] — the ambient health pill; `[]` hides it.
- `role` (string|false|null, default `null`) — The viewer's role chip; `false` hides it.
- `locale` (string|null, default `null`) — The current locale label on the language button.
- `localeHref` (string, default `#`) — Where the language button goes.
- `notificationsHref` (string, default `#`) — Where the bell goes.
- `unread` (int, default `0`) — Unread count; a dot appears when greater than zero.
- `navToggle` (bool, default `true`) — Render the mobile navigation toggle, which dispatches `page-bar:toggle-nav`.
- `top` ('0'|'14'|'16', default `0`) — Sticky offset in rem, to clear a fixed bar above.
## Use when
- Use to orient users and help them move across pages, sections, or commands.
- An admin shell whose sidebar carries session scope and whose bar carries page context.
- Record pages reached from a list, so previous/next stepping lives in the bar rather than inside the page.
## Avoid when
- Do not hide primary wayfinding in novelty interactions or deep nested structures if straightforward navigation would be clearer.
- A SaaS product header with account menus and a search box — use the `app-shell-bordered` header.
- The page needs no trail and no system status — `admin-page-header` alone is enough.
## Anti-patterns
- Hiding primary wayfinding in novelty interactions
## Rules
- Use the `<brok:admin-page-bar>` tag (or `<x-ui.admin-page-bar>`) 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-page-bar
- Registry JSON (files, props, contract): https://brokui.dev/r/open/admin-page-bar.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 |
|---|---|---|---|
| title | string | Orders | Plain title, shown when `trail` is empty. |
| trail | array | null | [['label','href' => null]] — ancestors carry an href, the current page does not. `null` renders a sample trail; `[]` renders the plain title. |
| record | array | null | null | ['previous' => ['label','href'], 'next' => ['label','href']] — record-to-record stepping. `null` renders a sample pair; `[]` renders none. |
| integrations | array | null | null | ['healthy','total','href'] — the ambient health pill; `[]` hides it. |
| role | string | false | null | null | The viewer's role chip; `false` hides it. |
| locale | string | null | null | The current locale label on the language button. |
| localeHref | string | # | Where the language button goes. |
| notificationsHref | string | # | Where the bell goes. |
| unread | int | 0 | Unread count; a dot appears when greater than zero. |
| navToggle | bool | true | Render the mobile navigation toggle, which dispatches `page-bar:toggle-nav`. |
| top | '0' | '14' | '16' | 0 | Sticky offset in rem, to clear a fixed bar above. |
Slots
recordNav— A custom stepper (a queue page with keyboard steps) in place of the previous/next pair.status— Replaces the integration pill and role chip.tools— Replaces the locale, theme and notification controls.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Ancestor crumbs and their separators hide below `sm` together, so a phone never shows a leading separator in front of the record name.
- The mobile toggle dispatches a window-level `page-bar:toggle-nav` event; the shell opens its navigation sheet on it.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized, alpine.
Guidance
Orient users and move between destinations.
Use when
- Use to orient users and help them move across pages, sections, or commands.
- An admin shell whose sidebar carries session scope and whose bar carries page context.
- Record pages reached from a list, so previous/next stepping lives in the bar rather than inside the page.
Avoid when
- Do not hide primary wayfinding in novelty interactions or deep nested structures if straightforward navigation would be clearer.
- A SaaS product header with account menus and a search box — use the `app-shell-bordered` header.
- The page needs no trail and no system status — `admin-page-header` alone is enough.
Use instead
- Visible links and local navigation
Anti-patterns
- Hiding primary wayfinding in novelty interactions
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- managed
- Focus
none
- The trail is a `breadcrumb` nav with the current page marked; stepping links carry the neighbouring record's name in their accessible label.
- 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-page-bar-{{ $record->id }}">
<div class="w-full">
<x-ui.admin.page-bar />
</div>
</div>
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
{{--
Page Bar — the bar that spans the main column only, never the sidebar.
Start side: where you are. End side: the state of the systems this admin
depends on, then the controls that change how you see it.
"Where you are" is said ONCE, here. A record page used to repeat it as a
breadcrumb row inside the page, directly under a bar that already
carried the record name, which cost a strip of vertical space on every
record and made the same fact compete with itself. The trail and the
record-to-record arrows live in this bar; the page starts at its own
header. A list page passes no trail and gets its plain title.
The integration pill is deliberate. An admin is a front end to payment,
accounting and carrier systems, and when one of them is behind, half the
figures on screen are provisional. Making that ambient means an operator
does not have to open a settings page to find out whether to trust the
payment column.
Scope (which store, which team) is NOT repeated here. It lives at the
top of the sidebar, always visible, never scrolled away.
Generalised from the Noord-C admin's `page-bar.blade.php`.
--}}
@props([
'title' => 'Orders',
// null (absent) renders a sample trail and stepping pair for the docs
// preview; a list page passes [] for the plain title.
'trail' => null,
'record' => null,
'integrations' => null,
'role' => null,
'locale' => null,
'localeHref' => '#',
'notificationsHref' => '#',
'unread' => 0,
'navToggle' => true,
'top' => '0',
])
@php
// Shapes documented in item.json knowledge.props:
// trail: [['label', 'href' => null]] — ancestors carry an href, the current page does not. Empty renders the plain title.
// record: ['previous' => ['label','href'], 'next' => ['label','href']] — record-to-record stepping for a page reached from a list.
// integrations: ['healthy' => 4, 'total' => 4, 'href' => '#'] — the ambient health pill; null hides it.
// top: '0'|'14'|'16' — sticky offset in rem, to clear a fixed bar above.
$trail ??= [['label' => __('Orders'), 'href' => '#'], ['label' => __('Order #1042')]];
$record ??= ['previous' => ['label' => __('Order #1041'), 'href' => '#'], 'next' => ['label' => __('Order #1043'), 'href' => '#']];
$trail = collect((array) $trail)->filter(fn ($crumb) => is_array($crumb) && ($crumb['label'] ?? '') !== '')->values();
$record = is_array($record) ? $record : [];
$hasRecordNav = isset($record['previous']) || isset($record['next']);
$integrations ??= ['healthy' => 4, 'total' => 4, 'href' => '#'];
$integrationsHealthy = $integrations !== [] && (int) ($integrations['healthy'] ?? 0) >= (int) ($integrations['total'] ?? 0);
$role ??= __('Administrator');
$locale ??= 'EN';
$topClass = match ((string) $top) {
'14' => 'top-14',
'16' => 'top-16',
default => 'top-0',
};
@endphp
<header
data-slot="page-bar"
data-surface="admin"
{{ $attributes->merge(['class' => 'sticky z-30 flex h-14 min-w-0 shrink-0 items-center gap-4 border-b border-border bg-background px-4 lg:px-6 '.$topClass]) }}
>
@if ($navToggle)
<x-ui.button variant="ghost" size="icon-sm" class="-ms-2 lg:hidden" x-data x-on:click="$dispatch('page-bar:toggle-nav')" :icon-label="__('Toggle navigation')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</x-ui.button>
@endif
@if ($trail->isEmpty())
<h2 data-slot="page-bar-title" class="min-w-0 truncate text-base font-semibold">{{ $title }}</h2>
@else
<x-ui.breadcrumb class="min-w-0">
<x-ui.breadcrumb.list class="flex-nowrap">
@foreach ($trail as $index => $crumb)
@php $isLast = $index === $trail->count() - 1; @endphp
@if ($index > 0)
{{-- Hidden with the ancestor it follows, or a phone shows a leading separator in front of the record name. --}}
<x-ui.breadcrumb.separator class="max-sm:hidden" />
@endif
<x-ui.breadcrumb.item :class="$isLast ? 'min-w-0' : 'min-w-0 max-sm:hidden'">
@if ($isLast)
<x-ui.breadcrumb.page class="truncate text-base font-semibold text-foreground">{{ $crumb['label'] }}</x-ui.breadcrumb.page>
@elseif (! empty($crumb['href']))
<x-ui.breadcrumb.link :href="$crumb['href']" class="truncate">{{ $crumb['label'] }}</x-ui.breadcrumb.link>
@else
<span class="truncate text-muted-foreground">{{ $crumb['label'] }}</span>
@endif
</x-ui.breadcrumb.item>
@endforeach
</x-ui.breadcrumb.list>
</x-ui.breadcrumb>
@isset($recordNav)
{{-- A queue page brings its own stepper: keyboard steps and an end-of-queue state the plain arrows do not have. --}}
{{ $recordNav }}
@elseif ($hasRecordNav)
{{-- Working a list means working the records in it. Without these the only route to the next record is back to the list and down to where you were. --}}
<nav data-slot="page-bar-record-nav" class="flex shrink-0 items-center gap-2" aria-label="{{ __('Record navigation') }}">
@foreach (['previous' => 'm15 18-6-6 6-6', 'next' => 'm9 18 6-6-6-6'] as $direction => $path)
@php $step = $record[$direction] ?? null; @endphp
@if ($step)
<x-ui.button variant="outline" size="icon-sm" :href="$step['href'] ?? '#'" :icon-label="$direction === 'previous' ? __('Previous: :name', ['name' => $step['label'] ?? '']) : __('Next: :name', ['name' => $step['label'] ?? ''])" :rel="$direction === 'previous' ? 'prev' : 'next'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 rtl:-scale-x-100" aria-hidden="true"><path d="{{ $path }}"/></svg>
</x-ui.button>
@endif
@endforeach
</nav>
@endisset
@endif
<div class="flex-1"></div>
@isset($status)
{{ $status }}
@else
@if ($integrations !== [])
<x-ui.badge
:tone="$integrationsHealthy ? 'positive' : 'attention'"
as="a"
:href="$integrations['href'] ?? '#'"
class="max-md:hidden"
>
<span @class(['size-2 shrink-0 rounded-full', 'bg-success' => $integrationsHealthy, 'bg-warning' => ! $integrationsHealthy]) aria-hidden="true"></span>
{{ __(':healthy of :total integrations healthy', ['healthy' => $integrations['healthy'] ?? 0, 'total' => $integrations['total'] ?? 0]) }}
</x-ui.badge>
@endif
@if ($role)
<x-ui.badge variant="outline" class="max-xl:hidden">
<span class="text-muted-foreground">{{ __('Role') }}</span>
<span>{{ $role }}</span>
</x-ui.badge>
@endif
@endisset
@isset($tools)
{{ $tools }}
@else
<x-ui.button variant="outline" size="sm" :href="$localeHref" class="max-sm:hidden" :aria-label="__('Change language (:locale)', ['locale' => $locale])">
{{ $locale }}
</x-ui.button>
<x-ui.theme-switcher size="sm" :labels="false" />
<x-ui.button variant="outline" size="icon-sm" :href="$notificationsHref" class="relative" :icon-label="trans_choice(':count unread notification|:count unread notifications', (int) $unread, ['count' => $unread])">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>
@if ((int) $unread > 0)
<span class="absolute end-2 top-2 size-2 rounded-full bg-destructive" aria-hidden="true"></span>
@endif
</x-ui.button>
@endisset
</header>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.2 - 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