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.
Preview
<x-ui.system-banner :fixed="false" :text="__('Development Mode')" :icon="true" />
Variant options
Size options
Appearance options
Align options
Installation
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.
-
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.
# 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
<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
{{-- 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
<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>
{{-- 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>
{{-- 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
Props
| 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.
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
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
- Theming hooks
Accessibility
- 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-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="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.
<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.
{{--
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