Skip to content
Brok UI

Loading…

No results

Page Bar

Open source

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.

Version
v1.0.2
Stability
stable
License
MIT
Related
Page Header
Breadcrumb
Theme Switcher

Preview

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

Installation

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

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

  • blade resources/views/components/ui/admin/page-bar.blade.php
Registry dependencies
button breadcrumb badge theme-switcher
Packages
composer: jml/brok:^0.2
npm: alpinejs

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-page-bar.md
# 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

manifest knowledge + registry-derived coverage

Props

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

page-bar page-bar-record-nav page-bar-title

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

Navigation and orientation

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
page-bar page-bar-title page-bar-record-nav
Theming hooks
page-bar

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
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-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-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.

resources/views/components/ui/admin/page-bar.blade.php Blade
{{--
    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