Banners

Summary

A full-width page-level bar for system-wide state: maintenance windows, degraded environments, an engaged kill switch. Banners persist until closed. For transient feedback use Soma.toast(), for section-scoped notices use a message. The solid -critical form is reserved for must-see safety states. Static markup renders with the page; dynamic banners are built with Soma.banner() and announced to screen readers via a live-region role.

When to use

PatternUse it for
BannerWhole-page state the user should see wherever they are: scan failed, connection lost, maintenance tonight.
Banner -criticalSafety states only: kill switch engaged, emergency stop. Solid fill; don't dilute it with routine notices.
MessageFeedback about THIS section or form: validation summaries, contextual notices, inline in the content flow.
ToastTransient async events unrelated to what's on screen: deploy finished, budget warning.

Examples

Default

Maintenance tonight — 22:00–23:00 UTC; deploys are queued during the window.

The default look: the info tint, icon info.

Success

Migration complete — all 14 projects moved to the new billing engine.

-success, icon check.

Warning, with action

Scan failed — the 02:00 scheduled scan of prod-web did not complete.

-warning; a trailing action button sits beside the content.

Danger

Environment degraded — us-east is refusing new deployments.

-danger, icon circle-alert.

Critical

Kill switch engaged — all agent actions are paused at project scope.

The solid -critical fill, reserved for safety states (kill switch, emergency stop), icon stop.

Without an icon

Read-only mode — changes made during the audit window will not be saved.

The icon span is optional; the content simply starts at the inline edge.

Dismissable

Auto-init wires the close button:

Certificate for gitlab.nsysware.com expires in 14 days.

Click the ×; no JS wiring is needed on your side.

Dynamic

Soma.banner() prepends to the page top:

Scroll to the top of the page to see them. The warning and markup banners use close: 'manual' for a ×; the critical banner shows the enforcement: it never renders a close button (the option is ignored on 'critical'), so the demo clears it with the handle's .close(). The third passes bodyHtml. Closed so far: (counted via on('close'); programmatic closes count too).

Placement & stacking

Static banners belong at the top of the region they describe, usually the first child of the page content. The component adds a top margin between stacked banners and drops it on the first one (:first-child), so several render as a tidy stack. Dynamic banners are prepended to <body>, above the shell; the newest sits on top. Banners are not sticky by themselves. If one must survive scrolling, wrap it in a consumer-styled position: sticky container.

Live regions

Screen readers only announce live-region content that appears — so static banners, present at page load, get no role, while dynamic banners are announced: role="alert" (interrupts) for warning, danger and critical, role="status" (queues politely) for default and success. This mirrors the toast and message conventions.

HTML

The minimal banner — the tint, an optional icon, and the content region:

<div class="soma-banner">
  <span class="soma-icon soma-icon-info"></span>
  <div class="soma-banner-content">
    <strong>Maintenance tonight</strong> — 22:00–23:00 UTC.
  </div>
</div>

A trailing action button sits after the content (and before any close button):

<div class="soma-banner soma-banner-warning">
  <span class="soma-icon soma-icon-triangle-alert"></span>
  <div class="soma-banner-content">
    <strong>Scan failed</strong> — the 02:00 scheduled scan did not complete.
  </div>
  <button class="soma-button soma-button-compact">Details</button>
</div>

To make it dismissable, add the close button; auto-init binds it on page load, no JS on your side:

<div class="soma-banner soma-banner-warning">
  <span class="soma-icon soma-icon-triangle-alert"></span>
  <div class="soma-banner-content">Certificate expires in 14 days.</div>
  <button class="soma-banner-close" aria-label="Dismiss">
    <span class="soma-icon soma-icon-close"></span>
  </button>
</div>

The solid -critical form for safety states. Links inside inherit the fill's text color and are underlined:

<div class="soma-banner soma-banner-critical">
  <span class="soma-icon soma-icon-stop"></span>
  <div class="soma-banner-content">
    <strong>Kill switch engaged</strong> — all agent actions are paused.
    <a href="/status">Status page</a>
  </div>
</div>

Recommended icon per variant: default → info, success → check, warning → triangle-alert, danger → circle-alert, critical → stop. The icon span may be omitted entirely.

CSS classes

ClassEffect
.soma-bannerFull-width page-level bar, info tint by default. Stacked banners get a top margin (dropped on the first).
.soma-banner-warning / -danger / -successTinted semantic variants.
.soma-banner-criticalSolid danger fill for safety states; links inside inherit the text color and are underlined.
.soma-banner-contentThe flexible text region; <strong> inside renders semibold. Trailing action buttons sit beside it.
.soma-banner-closeOptional dismiss button (icon inside); auto-init binds it on page load.

JavaScript

Factory + options

MemberDescription
Soma.banner(elOrSelector)Wrap an existing .soma-banner: get-or-create singleton per element. Throws when nothing matches or the class is missing. (Auto-init already wraps every static banner that has a .soma-banner-close.)
Soma.banner(opts)Build a banner dynamically, prepend it to <body> with a live-region role, and return the instance.
opts.appearance'default' (info tint) | 'warning' | 'danger' | 'success' | 'critical'. Unknown values throw.
opts.bodyThe content, inserted as text — never parsed as HTML. Required unless bodyHtml is given.
opts.bodyHtmlMarkup opt-in for content you control (bold, links). Wins over body when both are set.
opts.iconGlyph name (e.g. 'triangle-alert'); renders the leading icon span. Omit for no icon.
opts.close'never' (default): no close button, the state persists until your code closes it. 'manual': renders a close button with a localised label (Soma.i18n key banner.dismiss). Ignored on appearance: 'critical': safety banners never render a ×; .close() still works programmatically.

Instance methods

MemberDescription
.close()Dispatch soma-banner-close, remove the banner from the DOM, then fire on('close') listeners. A no-op when the banner is already gone.
.on('close', fn)Run fn when the banner closes, whether by the × or programmatically.
.off('close', fn)Remove a listener: pass the same function reference given to .on().

All three methods return the instance, so calls chain. The underlying DOM event is a bubbling soma-banner-close CustomEvent dispatched on the banner element just before removal, usable with addEventListener for delegated listening.

Build a banner for a runtime condition and react when the user closes it:

const b = Soma.banner({
  appearance: 'warning',
  body: 'Connection lost — retrying…',
  icon: 'triangle-alert',
  close: 'manual',
});
b.on('close', () => reconnect());

Wrap a static banner to close it programmatically, e.g. dropping the maintenance notice the moment the window ends:

const maint = Soma.banner('#maintenance-banner');
maintenanceWindow.finished.then(() => maint.close());

bodyHtml when the content needs markup — trusted content only, never user input:

Soma.banner({
  bodyHtml: '<strong>Read-only mode</strong> — changes will not be saved.',
  icon: 'info',
  close: 'manual',
});

Delegated listening via the bubbling CustomEvent (no instance reference needed):

document.addEventListener('soma-banner-close', (e) => {
  console.log('banner closed:', e.target.textContent.trim());
});

The live examples above, exactly as this page wires them:

const closedEl = document.getElementById('banner-closed');
let closed = 0;
const countClose = (b) => b.on('close', () => { closedEl.textContent = String(++closed); });

document.getElementById('banner-warning').addEventListener('click', () =>
  countClose(Soma.banner({
    appearance: 'warning',
    body: 'Connection lost — retrying…',
    icon: 'triangle-alert',
    close: 'manual',
  })));
let criticalBanner = null;
document.getElementById('banner-critical').addEventListener('click', () => {
  criticalBanner = countClose(Soma.banner({
    appearance: 'critical',
    body: 'Kill switch engaged — all agent actions are paused.',
    icon: 'stop',
    // no close option: 'manual' would be ignored on 'critical' anyway
  }));
});
document.getElementById('banner-critical-clear').addEventListener('click', () => {
  if (criticalBanner) { criticalBanner.close(); criticalBanner = null; }
});
document.getElementById('banner-html').addEventListener('click', () =>
  countClose(Soma.banner({
    bodyHtml: '<strong>Read-only mode</strong> — changes will not be saved.',
    icon: 'info',
    close: 'manual',
  })));