Banner
Full-bleed system notice bar for page-level announcements
Banner is the app-frame notice bar: maintenance windows, degraded integrations, trial expiry, read-only mode. It spans the full width of the shell — sitting above or inside AdminShell chrome — so a single message reaches every screen without stealing space from the working area.
It is deliberately not an Alert. Alert is inline and content-scoped: it explains the form you are looking at, keeps a card radius, and stays where you put it. Banner is chrome — denser vertically, square-cornered with a bottom border only, optionally sticky, and dismissible by the operator.
Import
import { Banner } from '@nim-ui/components';Playground
Change the tone and watch the ARIA role follow: warning and error render role="alert" so screen readers interrupt; everything else is a polite role="status".
<div className="w-full overflow-hidden rounded-md border border-neutral-200 dark:border-neutral-800"> <Banner tone="warning" title="Scheduled maintenance"> Order sync pauses Sunday 02:00–04:00 UTC. </Banner> <Banner tone="info" title="Trial ends in 5 days" dismissible action={<Button size="sm">Upgrade plan</Button>} > Add billing details to keep fulfillment running. </Banner> <Banner tone="success" title="Warehouse sync restored" dismissible /> <Banner align="center" icon={null}> Read-only mode — you are viewing a historical snapshot. </Banner> </div>
Tones
neutral uses the standard surface treatment for routine announcements. The four status tones are soft tonal, matching Badge and StatusPill so a banner reads as the same severity language used elsewhere in the app.
Release 2026.7 is live
New export limits
Warehouse sync restored
Scheduled maintenance
Payment provider degraded
Code
<Banner title="Release 2026.7 is live">Changelog is available in Settings.</Banner>
<Banner tone="info" title="New export limits">CSV exports now cap at 50k rows.</Banner>
<Banner tone="success" title="Warehouse sync restored">All queues are draining normally.</Banner>
<Banner tone="warning" title="Scheduled maintenance">Order sync pauses Sunday 02:00–04:00 UTC.</Banner>
<Banner tone="error" title="Payment provider degraded">Captures are queued and will retry automatically.</Banner>Action and dismissal
Give the operator one way out: an action that resolves the notice, a dismiss button that hides it, or both. Dismissal is uncontrolled — the banner calls onDismiss and removes itself from the page, so persisting the choice is your job.
Trial ends in 5 days
Add billing details to keep fulfillment running.
3 payouts failed
Bank details were rejected by the provider.
Code
<Banner
tone="info"
title="Trial ends in 5 days"
dismissible
onDismiss={() => localStorage.setItem('trial-banner', 'dismissed')}
action={<Button size="sm">Upgrade plan</Button>}
>
Add billing details to keep fulfillment running.
</Banner>
<Banner tone="error" title="3 payouts failed" action={<Link href="#">Review payouts</Link>}>
Bank details were rejected by the provider.
</Banner>Because dismissal unmounts the banner, the same element will not reappear when its props change. When a new notice must surface after a prior dismissal, give the banner a key tied to the notice id (or conditionally render it) so React mounts a fresh instance.
Alignment and icons
align="center" suits short, whole-app statements such as read-only mode. Pass icon={null} when the copy carries the meaning on its own, or supply your own node for product-specific marks.
Impersonating operator@acme.io
Legacy API deprecated
Code
<Banner align="center" icon={null}>
Read-only mode — you are viewing a historical snapshot.
</Banner>
<Banner tone="info" align="center" title="Impersonating operator@acme.io" dismissible />
<Banner tone="warning" icon={null} title="Legacy API deprecated">
Migrate to v3 before 31 August.
</Banner>Sticky chrome
sticky pins the banner to the top of the viewport at z-30 — above the AdminShell header — so an outage notice survives a long queue scroll. Reserve it for notices that stay true while the operator works.
Payment provider degraded
Scroll this panel — the banner stays pinned.
Order #10241 · captured
Order #10242 · queued
Order #10243 · queued
Order #10244 · queued
Order #10245 · queued
Order #10246 · queued
Code
<Banner sticky tone="error" title="Payment provider degraded">
Captures are queued and will retry automatically.
</Banner>Props
| Name | Type | Default | Description |
|---|---|---|---|
tone | 'neutral' | 'info' | 'success' | 'warning' | 'error' | 'neutral' | Severity of the notice; drives the tonal surface and the ARIA role |
title | React.ReactNode | - | Headline of the notice, rendered in medium weight |
children | React.ReactNode | - | Supporting copy shown beneath the title |
icon | React.ReactNode | tone icon | Leading icon slot; defaults to a decorative tone icon, pass null to suppress |
action | React.ReactNode | - | Trailing action slot — a Button or Link that resolves the notice |
dismissible | boolean | false | Render a dismiss button that unmounts the banner |
onDismiss | () => void | - | Called when the operator dismisses the banner; persist the choice here |
dismissLabel | string | 'Dismiss' | Accessible name for the dismiss button |
sticky | boolean | false | Pin the banner to the top of the viewport above app chrome |
align | 'start' | 'center' | 'start' | Horizontal alignment of the banner content |
className | string | - | Additional CSS classes to apply |
Accessibility
neutral,info, andsuccessrenderrole="status"(polite) so routine notices do not interrupt;warninganderrorrenderrole="alert"(assertive) so urgent notices do.- Mount urgent banners in response to the event rather than rendering them hidden — a live region only announces content that arrives after it is on the page.
- The dismiss button has an accessible name (
dismissLabel, default "Dismiss") and the standardfocus-visiblering, so it is reachable and visible via keyboard. - Default tone icons are
aria-hidden— severity is carried by the copy and the role, never by color alone. A customiconkeeps its own semantics, so give a meaningful mark analtoraria-label(and mark a purely decorative onearia-hiddenyourself). - Dismissing moves focus to the document body because the button unmounts with the banner; if the banner sits mid-page, move focus somewhere deliberate in
onDismiss. - Place the banner early in the DOM (above the header) so tab order reaches the notice and its action before the page content.
Best Practices
Do
- Use Banner for app-wide state, and Alert for messages scoped to one form, card, or panel
- Keep the copy to one line of
titleplus one line of supporting text — it is chrome, not a document - Pair
dismissiblewith persistence inonDismissso the notice does not return on every route change - Reserve
errorandwarningfor notices an operator must act on, sorole="alert"keeps its meaning - Give the banner a
keytied to the notice id when a new message must appear after a prior dismissal
Don't
- Stack more than one banner — the second one is already noise; queue them or promote the urgent one
- Use Banner for transient confirmations ("Saved") — that is Toast
- Round the corners or add a shadow to make it look like a card; the flat bottom border is what makes it read as chrome
- Make a banner dismissible when the condition is still true and unresolvable by the operator
- Use
stickyfor notices that stop being true as the operator scrolls or navigates
Related Components
- Alert - Inline, content-scoped feedback message
- Toast - Transient confirmation of an action
- AdminShell - App frame the banner sits inside
- StatusPill - Compact record-level status