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
| Pattern | Use it for |
|---|---|
| Banner | Whole-page state the user should see wherever they are: scan failed, connection lost, maintenance tonight. |
Banner -critical | Safety states only: kill switch engaged, emergency stop. Solid fill; don't dilute it with routine notices. |
| Message | Feedback about THIS section or form: validation summaries, contextual notices, inline in the content flow. |
| Toast | Transient async events unrelated to what's on screen: deploy finished, budget warning. |
Examples
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
| Class | Effect |
|---|---|
.soma-banner | Full-width page-level bar, info tint by default. Stacked banners get a top margin (dropped on the first). |
.soma-banner-warning / -danger / -success | Tinted semantic variants. |
.soma-banner-critical | Solid danger fill for safety states; links inside inherit the text color and are underlined. |
.soma-banner-content | The flexible text region; <strong> inside renders semibold. Trailing action buttons sit beside it. |
.soma-banner-close | Optional dismiss button (icon inside); auto-init binds it on page load. |
JavaScript
Factory + options
| Member | Description |
|---|---|
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.body | The content, inserted as text — never parsed as HTML. Required unless bodyHtml is given. |
opts.bodyHtml | Markup opt-in for content you control (bold, links). Wins over body when both are set. |
opts.icon | Glyph 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
| Member | Description |
|---|---|
.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',
})));