Skip to content
Brok UI

Loading…

No results

System Banner

Open source

A full-width banner for environment, system-status, maintenance, or promo announcements — fixed to the top of the page or inline, optionally with a leading icon, trailing actions, a dismissible close button that can persist its dismissal for the session, and a soft contained callout skin for in-page use.

Version
v1.4.2
Stability
stable
License
MIT
Related
Alert
Attention Banners
Toast

Preview

Development Mode
Variant
Size
Appearance
Align
previews.components.system-banner.default.blade.php Blade
<x-ui.system-banner :fixed="false" :text="__('Development Mode')" :icon="true" />

Variant options

Warning Current
Info Current
Destructive Current
Success Current
Muted Current
Primary Current
Accent Current

Size options

Xs Current
Sm Current
Md Current
Lg Current

Appearance options

Soft Current
Solid Current
Inverse Current
Outline Current

Align options

Start Current
Center Current
End Current

Installation

terminal
php artisan ui:add system-banner

Registry contract

php artisan ui:add system-banner 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/system-banner.blade.php
Registry dependencies
None — installs on its own.
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.

system-banner.md
# Brok UI: System Banner (`system-banner`)

A full-width banner for environment, system-status, maintenance, or promo announcements — fixed to the top of the page or inline, optionally with a leading icon, trailing actions, a dismissible close button that can persist its dismissal for the session, and a soft contained callout skin for in-page use.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add system-banner
```

## Usage

```blade
<x-ui.system-banner :fixed="false" :text="__('Development Mode')" :icon="true" />
```

## Props

- `text` (string, default `Development Mode`) — Message shown when no default slot is supplied.
- `variant` (warning|info|success|destructive|muted|primary|accent, default `warning`) — Tone: info, success, warning, destructive, muted, primary, or accent.
- `size` (xs|sm|md|lg, default `xs`) — Bar height and padding: xs, sm, md, or lg.
- `show` (bool, default `true`) — Renders the banner at all; false renders nothing.
- `fixed` (bool, default `true`) — Pins the bar to the top of the viewport; false renders it inline.
- `contained` (bool, default `false`) — Renders a rounded, bordered callout in the page flow (forces the soft appearance); pair with fixed=false.
- `appearance` (string, default `soft`) — Skin: soft (tinted surface), solid (edge-to-edge fill), inverse (neutral dark bar), or outline (card-coloured bar with a hairline).
- `icon` (bool, default `false`) — Shows a leading variant icon.
- `label` (mixed|null, default `null`) — Small pill shown before the message, such as "Beta" or "v2.0".
- `href` (mixed|null, default `null`) — Turns the message into a link with a trailing arrow.
- `dismissible` (bool, default `false`) — Renders a keyboard-operable close button.
- `dismissKey` (mixed|null, default `null`) — Key that persists the dismissal in sessionStorage for the tab session; scope it to the condition being reported, not the banner's static shape.
- `align` (string, default `center`) — Layout: center, or between (message on the start side, actions/close on the end side).
- `role` (string, default `status`) — ARIA live-region role: status, alert, or region.

## Use when

- Use for important messages that should stay visible long enough to read and act on.
- Announcing an environment label, system incident, maintenance window, or promo across the full width of the app, fixed to the top or inline.
- Needing a dismissible bar whose dismissal can persist for the session via a caller-supplied dismissKey.

## Avoid when

- Do not downgrade important guidance into transient messaging users can miss.
- Showing a short, in-page callout scoped to one section; use alert instead.
- Wanting a transient, auto-clearing message; use toaster instead.

## Anti-patterns

- Downgrading important outcomes to transient feedback

## Rules

- Use the `<brok:system-banner>` tag (or `<x-ui.system-banner>`) 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/system-banner
- Registry JSON (files, props, contract): https://brokui.dev/r/open/system-banner.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

action.blade.php Blade
<div class="flex w-full flex-col gap-2">
    <x-ui.system-banner :fixed="false" variant="info" size="sm" :icon="true" :dismissible="true" align="between">
        {{ __('A new version of the dashboard is available.') }}
        <x-ui.button size="sm" variant="outline">{{ __('Update now') }}</x-ui.button>
    </x-ui.system-banner>

    <x-ui.system-banner :fixed="false" variant="warning" size="sm" :icon="true" align="between">
        {{ __('You are running low on storage.') }}
        <a href="#" class="font-semibold underline underline-offset-4">{{ __('Manage') }}</a>
    </x-ui.system-banner>
</div>
appearances.blade.php Blade
{{-- Beyond tone: an inverse announcement bar with a label pill and a link, an
     outline notice, and a contained callout in the flow of a page. --}}
<div class="flex w-full flex-col gap-2">
    <x-ui.system-banner :fixed="false" appearance="inverse" size="sm" :label="__('New')" href="#">
        {{ __('Brok 2.0 ships with 80 new blocks') }}
    </x-ui.system-banner>
    <x-ui.system-banner :fixed="false" appearance="outline" size="sm" :label="__('Beta')" align="between" :dismissible="true">
        {{ __('You are previewing the new dashboard.') }}
        <a href="#" class="font-semibold underline underline-offset-4">{{ __('Send feedback') }}</a>
    </x-ui.system-banner>
    <x-ui.system-banner :fixed="false" :contained="true" variant="warning" size="sm" :icon="true" align="between">
        {{ __('Your trial ends in 3 days.') }}
        <x-ui.button size="sm" variant="outline">{{ __('Upgrade') }}</x-ui.button>
    </x-ui.system-banner>
</div>
dismissible.blade.php Blade
<div class="flex w-full flex-col gap-2">
    <x-ui.system-banner :fixed="false" variant="info" size="sm" :icon="true" :dismissible="true" :text="__('Scheduled maintenance tonight at 02:00 UTC.')" />
    <x-ui.system-banner :fixed="false" variant="success" size="sm" :icon="true" :dismissible="true" :text="__('Your changes have been saved.')" />
</div>
sticky.blade.php Blade
{{-- The sticky top-bar idiom, shown inside a faux viewport so the bar stays
     scoped to the preview instead of pinning to the page. In your app, use the
     default :fixed="true" to pin it to the top of the viewport. --}}
<div class="relative h-40 w-full overflow-hidden rounded-lg border border-border bg-background">
    <x-ui.system-banner
        :fixed="false"
        variant="destructive"
        size="sm"
        :icon="true"
        :dismissible="true"
        :text="__('We are investigating an incident affecting some services.')"
        class="absolute inset-x-0 top-0 z-10"
    />
    <div class="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
        {{ __('Page content scrolls beneath the pinned bar.') }}
    </div>
</div>
variants.blade.php Blade
{{-- Four tones on the quiet soft skin; `appearance="solid"` for the loud fill. --}}
<div class="flex w-full flex-col gap-2">
    <x-ui.system-banner :fixed="false" variant="warning" size="sm" :icon="true" :text="__('Staging environment')" />
    <x-ui.system-banner :fixed="false" variant="info" size="sm" :icon="true" :text="__('Read-only mode until 14:00 UTC')" />
    <x-ui.system-banner :fixed="false" variant="destructive" size="sm" :icon="true" :text="__('Incident in progress')" />
    <x-ui.system-banner :fixed="false" variant="success" size="sm" :icon="true" :text="__('All migrations applied')" />
    <x-ui.system-banner :fixed="false" variant="destructive" appearance="solid" size="sm" :icon="true" :text="__('Incident in progress (solid)')" />
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
text string Development Mode Message shown when no default slot is supplied.
variant warning | info | success | destructive | muted | primary | accent warning Tone: info, success, warning, destructive, muted, primary, or accent.
size xs | sm | md | lg xs Bar height and padding: xs, sm, md, or lg.
show bool true Renders the banner at all; false renders nothing.
fixed bool true Pins the bar to the top of the viewport; false renders it inline.
contained bool false Renders a rounded, bordered callout in the page flow (forces the soft appearance); pair with fixed=false.
appearance string soft Skin: soft (tinted surface), solid (edge-to-edge fill), inverse (neutral dark bar), or outline (card-coloured bar with a hairline).
icon bool false Shows a leading variant icon.
label mixed | null null Small pill shown before the message, such as "Beta" or "v2.0".
href mixed | null null Turns the message into a link with a trailing arrow.
dismissible bool false Renders a keyboard-operable close button.
dismissKey mixed | null null Key that persists the dismissal in sessionStorage for the tab session; scope it to the condition being reported, not the banner's static shape.
align string center Layout: center, or between (message on the start side, actions/close on the end side).
role string status ARIA live-region role: status, alert, or region.

Slots

  • default — Message and optional trailing actions; overrides the text prop when supplied.

Data slots

Stable hooks for CSS overrides and browser tests.

system-banner system-banner-dismiss system-banner-label system-banner-link

Behavior

  • Dismissal only persists when both dismissible is true and dismissKey is set; otherwise the close button only hides the banner for the current page view.
  • contained forces the soft appearance and adds rounded corners and a border for an in-page callout look.
  • Renders a fixed or inline full-width bar (or, with contained, a soft bordered callout); an optional close button hides it via Alpine state, persisted to sessionStorage when dismissKey is set.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Persistent message and notification

Keep an important message visible near its context.

Use when

  • Use for important messages that should stay visible long enough to read and act on.
  • Announcing an environment label, system incident, maintenance window, or promo across the full width of the app, fixed to the top or inline.
  • Needing a dismissible bar whose dismissal can persist for the session via a caller-supplied dismissKey.

Avoid when

  • Do not downgrade important guidance into transient messaging users can miss.
  • Showing a short, in-page callout scoped to one section; use alert instead.
  • Wanting a transient, auto-clearing message; use toaster instead.

Use instead

  • Toast for low-priority confirmation

Anti-patterns

  • Downgrading important outcomes to transient feedback
Anatomy
root system-banner-label system-banner-link system-banner-dismiss
Theming hooks
system-banner

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • The root's role follows the role prop (status by default) so assistive tech announces the message as a live region.
  • The dismiss button carries a localized "Dismiss" aria-label; decorative icons and separators are aria-hidden.
  • 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="system-banner-{{ $record->id }}">
    <x-ui.system-banner :fixed="false" :text="__('Development Mode')" :icon="true" />
</div>

Validation

Validation support: native. Keep the error message connected with aria-describedby.

livewire-form.blade.php Blade
<form wire:submit="save" class="space-y-2">
    <brok:system-banner
        wire:model="value"
        :aria-invalid="$errors->has('value') ? 'true' : 'false'"
        aria-describedby="value-error"
    />

    @error('value')
        <p id="value-error" role="alert">{{ $message }}</p>
    @enderror

    <brok:button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove>Save</span>
        <span wire:loading>Saving…</span>
    </brok:button>
</form>

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/system-banner.blade.php Blade
{{--
    System Banner — page/app-level announcement or environment/status bar.

    Differs from <x-ui.alert> (an in-page callout): this is the full-width bar
    pinned to the top of the app (or rendered inline) for environment labels,
    incidents, maintenance windows, and promotions.

    Props:
      text         (string) — message shown when no slot is supplied   ['Development Mode']
      variant      (string) — info | success | warning | destructive | muted | primary | accent  ['warning']
      appearance   (string) — soft (tinted surface, hairline, tone text — the quiet default)
                              | solid (loud edge-to-edge fill) | inverse (neutral dark bar)
                              | outline (card-coloured bar with a hairline)   ['soft']
      size         (string) — xs | sm | md | lg                        ['xs']
      show         (bool)   — render the banner at all                 [true]
      fixed        (bool)   — pin to the top of the viewport (sticky bar) vs inline  [true]
      contained    (bool)   — rounded, bordered callout in the flow of a page
                              (implies the soft appearance) — pair with :fixed="false"   [false]
      icon         (bool)   — show a leading variant icon              [false]
      label        (string|null) — a small pill before the message ("Beta", "v2.0", "New")   [null]
      href         (string|null) — the message becomes a link with a trailing arrow   [null]
      dismissible  (bool)   — render a keyboard-operable close button   [false]
      dismissKey   (string|null) — remembers the dismissal in sessionStorage for the
                              tab session. Derive it from the CONDITION being reported,
                              never from the banner's own static shape.   [null]
      align        (string) — center | between (between = message left, actions/close right)  ['center']
      role         (string) — ARIA live-region role: status | alert | region  ['status']

    Compose actions (button/link) by passing a slot — the bar lays them out
    after the message when align="between".
--}}
@props([
    'text' => 'Development Mode',
    'variant' => 'warning',
    'size' => 'xs',
    'show' => true,
    'fixed' => true,
    'contained' => false,
    'appearance' => 'soft',
    'icon' => false,
    'label' => null,
    'href' => null,
    'dismissible' => false,
    'dismissKey' => null,
    'align' => 'center',
    'role' => 'status',
])

@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $variant = $styles['normalizeVariant']($variant);
    $banner = $styles['system-banner'];

    // Variant classes come from the shared recipe; `primary`/`accent` fall back to
    // inline token classes so the bar still themes correctly even on an older recipe.
    $size = in_array($size, ['xs', 'sm', 'md', 'lg'], true) ? $size : 'xs';
    $variant = in_array($variant, ['info', 'success', 'warning', 'destructive', 'muted', 'primary', 'accent'], true) ? $variant : 'warning';
    $appearance = in_array($appearance, ['soft', 'solid', 'inverse', 'outline'], true) ? $appearance : 'soft';
    if ($contained) {
        $appearance = 'soft';
    }
    $skin = match ($appearance) {
        'solid' => $banner['variants'][$variant] ?? $banner['variants']['warning'],
        'inverse', 'outline' => $banner['appearances'][$appearance],
        default => trim($banner['appearances']['soft'].' '.($banner['variantsSoft'][$variant] ?? $banner['variantsSoft']['warning'])),
    };

    $position = $fixed ? $banner['positions']['fixed'] : $banner['positions']['inline'];

    // align: between spreads message and trailing actions/close to the edges.
    $layout = $align === 'between' ? 'justify-between gap-3' : 'justify-center gap-2';

    $classes = $banner['base'].' '.$position.' '.$skin.' '.($banner['sizes'][$size] ?? $banner['sizes']['xs']).' '.$layout;
    if ($contained) {
        $classes .= ' rounded-md border';
    }

    // Dismissal only persists when both a key is given AND the banner is
    // actually dismissible — a key with no close button would do nothing.
    // The component does not invent a key from `variant`/`text`/etc.: a
    // default derived from the banner's own shape would dismiss every banner
    // that looks like it forever, regardless of what the message says. The
    // caller supplies one instead, scoped to the fact being reported, so a
    // changed condition (a different id/count/hash) naturally gets a new key
    // and the banner reappears.
    $persistDismiss = $dismissible && $dismissKey !== null;
    $storageKey = $persistDismiss ? 'ui-system-banner:'.$dismissKey : null;

    // Leading icon per variant (decorative — message carries the meaning).
    $icons = [
        'info' => 'M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM8 4a.875.875 0 1 1 0 1.75A.875.875 0 0 1 8 4Zm.75 8a.75.75 0 0 1-1.5 0V7.5a.75.75 0 0 1 1.5 0V12Z',
        'success' => 'M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm3.03 4.97-3.75 3.75a.75.75 0 0 1-1.06 0L4.97 8.78a.75.75 0 1 1 1.06-1.06l.72.72 3.22-3.22a.75.75 0 1 1 1.06 1.06Z',
        'warning' => 'M7.13 2.6a1 1 0 0 1 1.74 0l5.2 9.15A1 1 0 0 1 13.2 13.3H2.8a1 1 0 0 1-.87-1.55L7.13 2.6ZM8 5.5a.75.75 0 0 0-.75.75V9a.75.75 0 0 0 1.5 0V6.25A.75.75 0 0 0 8 5.5Zm0 5.25a.875.875 0 1 0 0 1.75.875.875 0 0 0 0-1.75Z',
        'destructive' => 'M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM8 4.5a.75.75 0 0 1 .75.75V8.5a.75.75 0 0 1-1.5 0V5.25A.75.75 0 0 1 8 4.5Zm0 6a.875.875 0 1 0 0 1.75A.875.875 0 0 0 8 10.5Z',
    ];
    $iconPath = $icons[$variant] ?? $icons['info'];
@endphp

@if ($show)
    <div
        @if ($dismissible)
            @if ($persistDismiss)
                x-data="{ shown: (function () { try { return sessionStorage.getItem({{ Illuminate\Support\Js::from($storageKey) }}) !== 'dismissed'; } catch (e) { return true; } })() }"
                x-cloak
            @else
                x-data="{ shown: true }"
            @endif
            x-show="shown"
        @endif
        data-slot="system-banner"
        data-variant="{{ $variant }}"
        data-appearance="{{ $appearance }}"
        @if ($contained) data-contained="true" @endif
        role="{{ $role }}"
        {{ $attributes->merge(['class' => $classes]) }}
    >
        @if ($align === 'between')
            {{-- The slot holds message AND actions: the message span spreads them
                 (text at the start, actions at the end) and the close button follows. --}}
            <div class="flex min-w-0 flex-1 items-center gap-2">
        @endif

        @if ($icon)
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4 shrink-0" aria-hidden="true">
                <path d="{{ $iconPath }}" />
            </svg>
        @endif

        @if ($label !== null)
            <span data-slot="system-banner-label" class="inline-flex shrink-0 items-center rounded-full bg-current/10 px-2 py-0.5 text-2xs font-semibold uppercase tracking-wide">{{ $label }}</span>
        @endif

        @if ($href !== null)
            <a href="{{ $href }}" data-slot="system-banner-link" class="inline-flex min-w-0 items-center gap-1 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:rounded-sm focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-current">
                <span class="min-w-0">{{ $slot->isEmpty() ? $text : $slot }}</span>
                <svg class="size-4 shrink-0 rtl:-scale-x-100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
            </a>
        @else
            <span class="{{ $align === 'between' ? 'flex min-w-0 flex-1 flex-wrap items-center justify-between gap-3 text-start' : 'min-w-0' }}">{{ $slot->isEmpty() ? $text : $slot }}</span>
        @endif

        @if ($align === 'between')
            </div>
        @endif

        @if ($dismissible)
            <button
                type="button"
                @if ($persistDismiss)
                    x-on:click="shown = false; try { sessionStorage.setItem({{ Illuminate\Support\Js::from($storageKey) }}, 'dismissed'); } catch (e) {}"
                @else
                    x-on:click="shown = false"
                @endif
                @class([
                    'inline-flex size-6 shrink-0 items-center justify-center rounded-sm opacity-70 transition-[opacity,background-color] hover:bg-current/10 hover:opacity-100 focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-current motion-reduce:transition-none',
                    'ms-auto' => $align !== 'between',
                ])
                aria-label="{{ __('Dismiss') }}"
                data-slot="system-banner-dismiss"
            >
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4" aria-hidden="true">
                    <path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" />
                </svg>
            </button>
        @endif
    </div>
@endif

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.4.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